checkin
This commit is contained in:
31
README
31
README
@@ -0,0 +1,31 @@
|
||||
# Why are there two microcontrollers?
|
||||
|
||||
Cost and ease of development.
|
||||
|
||||
Both ESP8266 and ATTINY(2/4/8)5 modules can be had for around \$1 each, and
|
||||
there are retail programming jigs available. I have a kid now and don't want to
|
||||
spend my time making a programming board.
|
||||
|
||||
The ESP8266 alone cannot really wake from multiple sources and know trap the
|
||||
source of the interrupt- there is only one pin that can wake up the micro from
|
||||
deep sleep, and that same pin needs to be used if you want to use the RTC as a
|
||||
wake source. Sure, you could use an OR-gate and also wire each source to a GPIO,
|
||||
but you might miss the source by the time the ESP wakes up to check the pins.
|
||||
|
||||
Hence, the ATTINY is used to capture the wakeup and inform the ESP8266, who does
|
||||
the heavy lifting.
|
||||
|
||||
## What about ESP32?
|
||||
|
||||
The cheapest modules I can find at the time of writing are around \$3.50 apiece,
|
||||
and they can be power hungry (spikes up to 750mA compared to 400mA, from what I
|
||||
read).
|
||||
|
||||
## What about ESP32-S2?
|
||||
|
||||
They appear promising and are only \$2 per module on Digikey at the time of
|
||||
writing, however they are not supported by platformio yet.
|
||||
|
||||
## What about ...?
|
||||
|
||||
I probably wasn't aware that it exists.
|
||||
|
||||
292
ecad/IotButtonV0/IotButtonV0-cache.lib
Normal file
292
ecad/IotButtonV0/IotButtonV0-cache.lib
Normal file
@@ -0,0 +1,292 @@
|
||||
EESchema-LIBRARY Version 2.4
|
||||
#encoding utf-8
|
||||
#
|
||||
# Connector_Generic_MountingPin_Conn_01x05_MountingPin
|
||||
#
|
||||
DEF Connector_Generic_MountingPin_Conn_01x05_MountingPin J 0 40 Y N 1 F N
|
||||
F0 "J" 0 300 50 H V C CNN
|
||||
F1 "Connector_Generic_MountingPin_Conn_01x05_MountingPin" 50 -300 50 H V L CNN
|
||||
F2 "" 0 0 50 H I C CNN
|
||||
F3 "" 0 0 50 H I C CNN
|
||||
$FPLIST
|
||||
Connector*:*_1x??-1MP*
|
||||
$ENDFPLIST
|
||||
DRAW
|
||||
T 0 0 -265 15 0 1 1 Mounting Normal 0 C C
|
||||
S -50 -195 0 -205 1 1 6 N
|
||||
S -50 -95 0 -105 1 1 6 N
|
||||
S -50 5 0 -5 1 1 6 N
|
||||
S -50 105 0 95 1 1 6 N
|
||||
S -50 205 0 195 1 1 6 N
|
||||
S -50 250 50 -250 1 1 10 f
|
||||
P 2 1 1 6 -40 -280 40 -280 N
|
||||
X Pin_1 1 -200 200 150 R 50 50 1 1 P
|
||||
X Pin_2 2 -200 100 150 R 50 50 1 1 P
|
||||
X Pin_3 3 -200 0 150 R 50 50 1 1 P
|
||||
X Pin_4 4 -200 -100 150 R 50 50 1 1 P
|
||||
X Pin_5 5 -200 -200 150 R 50 50 1 1 P
|
||||
X MountPin MP 0 -400 120 U 50 50 1 1 P
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# Device_Battery_Cell
|
||||
#
|
||||
DEF Device_Battery_Cell BT 0 0 N N 1 F N
|
||||
F0 "BT" 100 100 50 H V L CNN
|
||||
F1 "Device_Battery_Cell" 100 0 50 H V L CNN
|
||||
F2 "" 0 60 50 V I C CNN
|
||||
F3 "" 0 60 50 V I C CNN
|
||||
DRAW
|
||||
S -90 70 90 60 0 1 0 F
|
||||
S -62 47 58 27 0 1 0 F
|
||||
P 2 0 1 0 0 30 0 0 N
|
||||
P 2 0 1 0 0 70 0 100 N
|
||||
P 2 0 1 10 20 135 60 135 N
|
||||
P 2 0 1 10 40 155 40 115 N
|
||||
X + 1 0 200 100 D 50 50 1 1 P
|
||||
X - 2 0 -100 100 U 50 50 1 1 P
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# Device_C_Small
|
||||
#
|
||||
DEF Device_C_Small C 0 10 N N 1 F N
|
||||
F0 "C" 10 70 50 H V L CNN
|
||||
F1 "Device_C_Small" 10 -80 50 H V L CNN
|
||||
F2 "" 0 0 50 H I C CNN
|
||||
F3 "" 0 0 50 H I C CNN
|
||||
$FPLIST
|
||||
C_*
|
||||
$ENDFPLIST
|
||||
DRAW
|
||||
P 2 0 1 13 -60 -20 60 -20 N
|
||||
P 2 0 1 12 -60 20 60 20 N
|
||||
X ~ 1 0 100 80 D 50 50 1 1 P
|
||||
X ~ 2 0 -100 80 U 50 50 1 1 P
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# Device_LED_ARGB
|
||||
#
|
||||
DEF Device_LED_ARGB D 0 0 Y N 1 F N
|
||||
F0 "D" 0 370 50 H V C CNN
|
||||
F1 "Device_LED_ARGB" 0 -350 50 H V C CNN
|
||||
F2 "" 0 -50 50 H I C CNN
|
||||
F3 "" 0 -50 50 H I C CNN
|
||||
$FPLIST
|
||||
LED*
|
||||
LED_SMD:*
|
||||
LED_THT:*
|
||||
$ENDFPLIST
|
||||
DRAW
|
||||
C 80 0 10 0 1 0 F
|
||||
T 0 -75 -250 50 0 0 0 B Normal 0 C C
|
||||
T 0 -75 -50 50 0 0 0 G Normal 0 C C
|
||||
T 0 -75 150 50 0 0 0 R Normal 0 C C
|
||||
S 50 250 50 250 0 1 0 N
|
||||
S 110 330 -110 -300 0 1 10 f
|
||||
P 2 0 1 0 -100 -200 50 -200 N
|
||||
P 2 0 1 8 -50 -150 -50 -250 N
|
||||
P 2 0 1 8 -50 50 -50 -50 N
|
||||
P 2 0 1 8 -50 250 -50 150 N
|
||||
P 2 0 1 0 50 200 -100 200 N
|
||||
P 2 0 1 0 100 0 -100 0 N
|
||||
P 4 0 1 0 50 -200 80 -200 80 200 50 200 N
|
||||
P 4 0 1 8 50 -150 50 -250 -50 -200 50 -150 N
|
||||
P 4 0 1 8 50 50 50 -50 -50 0 50 50 N
|
||||
P 4 0 1 8 50 250 50 150 -50 200 50 250 N
|
||||
P 5 0 1 0 -40 -150 20 -90 -10 -90 20 -90 20 -120 N
|
||||
P 5 0 1 0 -40 50 20 110 -10 110 20 110 20 80 N
|
||||
P 5 0 1 0 -40 250 20 310 -10 310 20 310 20 280 N
|
||||
P 5 0 1 0 0 -150 60 -90 30 -90 60 -90 60 -120 N
|
||||
P 5 0 1 0 0 50 60 110 30 110 60 110 60 80 N
|
||||
P 5 0 1 0 0 250 60 310 30 310 60 310 60 280 N
|
||||
X A 1 200 0 100 L 50 50 1 1 P
|
||||
X RK 2 -200 200 100 R 50 50 1 1 P
|
||||
X GK 3 -200 0 100 R 50 50 1 1 P
|
||||
X BK 4 -200 -200 100 R 50 50 1 1 P
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# Device_R_Small_US
|
||||
#
|
||||
DEF Device_R_Small_US R 0 10 N N 1 F N
|
||||
F0 "R" 30 20 50 H V L CNN
|
||||
F1 "Device_R_Small_US" 30 -40 50 H V L CNN
|
||||
F2 "" 0 0 50 H I C CNN
|
||||
F3 "" 0 0 50 H I C CNN
|
||||
$FPLIST
|
||||
R_*
|
||||
$ENDFPLIST
|
||||
DRAW
|
||||
P 5 1 1 0 0 0 40 -15 0 -30 -40 -45 0 -60 N
|
||||
P 5 1 1 0 0 60 40 45 0 30 -40 15 0 0 N
|
||||
X ~ 1 0 100 40 D 50 50 1 1 P
|
||||
X ~ 2 0 -100 40 U 50 50 1 1 P
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# Device_R_US
|
||||
#
|
||||
DEF Device_R_US R 0 0 N Y 1 F N
|
||||
F0 "R" 100 0 50 V V C CNN
|
||||
F1 "Device_R_US" -100 0 50 V V C CNN
|
||||
F2 "" 40 -10 50 V I C CNN
|
||||
F3 "" 0 0 50 H I C CNN
|
||||
$FPLIST
|
||||
R_*
|
||||
$ENDFPLIST
|
||||
DRAW
|
||||
P 2 0 1 0 0 -90 0 -100 N
|
||||
P 2 0 1 0 0 90 0 100 N
|
||||
P 5 0 1 0 0 -30 40 -45 0 -60 -40 -75 0 -90 N
|
||||
P 5 0 1 0 0 30 40 15 0 0 -40 -15 0 -30 N
|
||||
P 5 0 1 0 0 90 40 75 0 60 -40 45 0 30 N
|
||||
X ~ 1 0 150 50 D 50 50 1 1 P
|
||||
X ~ 2 0 -150 50 U 50 50 1 1 P
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# MCU_Microchip_ATtiny_ATtiny85-20SU
|
||||
#
|
||||
DEF MCU_Microchip_ATtiny_ATtiny85-20SU U 0 20 Y Y 1 F N
|
||||
F0 "U" -500 550 50 H V L BNN
|
||||
F1 "MCU_Microchip_ATtiny_ATtiny85-20SU" 100 -550 50 H V L TNN
|
||||
F2 "Package_SO:SOIJ-8_5.3x5.3mm_P1.27mm" 0 0 50 H I C CIN
|
||||
F3 "" 0 0 50 H I C CNN
|
||||
ALIAS ATtiny25-20SU ATtiny45V-10SU ATtiny45-20SU ATtiny85V-10SU ATtiny85-20SU
|
||||
$FPLIST
|
||||
SOIJ*5.3x5.3mm*P1.27mm*
|
||||
$ENDFPLIST
|
||||
DRAW
|
||||
S -500 -500 500 500 0 1 10 f
|
||||
X ~RESET~/PB5 1 600 -200 100 L 50 50 1 1 T
|
||||
X XTAL1/PB3 2 600 0 100 L 50 50 1 1 T
|
||||
X XTAL2/PB4 3 600 -100 100 L 50 50 1 1 T
|
||||
X GND 4 0 -600 100 U 50 50 1 1 W
|
||||
X AREF/PB0 5 600 300 100 L 50 50 1 1 T
|
||||
X PB1 6 600 200 100 L 50 50 1 1 T
|
||||
X PB2 7 600 100 100 L 50 50 1 1 T
|
||||
X VCC 8 0 600 100 D 50 50 1 1 W
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# RF_Module_ESP-12F
|
||||
#
|
||||
DEF RF_Module_ESP-12F U 0 20 Y Y 1 F N
|
||||
F0 "U" -500 750 50 H V L CNN
|
||||
F1 "RF_Module_ESP-12F" 500 750 50 H V R CNN
|
||||
F2 "RF_Module:ESP-12E" 0 0 50 H I C CNN
|
||||
F3 "" -350 100 50 H I C CNN
|
||||
ALIAS ESP-12F
|
||||
$FPLIST
|
||||
ESP?12*
|
||||
$ENDFPLIST
|
||||
DRAW
|
||||
S -500 700 500 -600 0 1 10 f
|
||||
X ~RST 1 -600 600 100 R 50 50 1 1 I
|
||||
X MISO 10 -600 -100 100 R 50 50 1 1 B
|
||||
X GPIO9 11 -600 -200 100 R 50 50 1 1 B
|
||||
X GPIO10 12 -600 -300 100 R 50 50 1 1 B
|
||||
X MOSI 13 -600 -400 100 R 50 50 1 1 B
|
||||
X SCLK 14 -600 -500 100 R 50 50 1 1 B
|
||||
X GND 15 0 -700 100 U 50 50 1 1 W
|
||||
X GPIO15 16 600 -300 100 L 50 50 1 1 B
|
||||
X GPIO2 17 600 400 100 L 50 50 1 1 B
|
||||
X GPIO0 18 600 600 100 L 50 50 1 1 B
|
||||
X GPIO4 19 600 200 100 L 50 50 1 1 B
|
||||
X ADC 2 -600 200 100 R 50 50 1 1 I
|
||||
X GPIO5 20 600 100 100 L 50 50 1 1 B
|
||||
X GPIO3/RXD 21 600 300 100 L 50 50 1 1 B
|
||||
X GPIO1/TXD 22 600 500 100 L 50 50 1 1 B
|
||||
X EN 3 -600 400 100 R 50 50 1 1 I
|
||||
X GPIO16 4 600 -400 100 L 50 50 1 1 B
|
||||
X GPIO14 5 600 -200 100 L 50 50 1 1 B
|
||||
X GPIO12 6 600 0 100 L 50 50 1 1 B
|
||||
X GPIO13 7 600 -100 100 L 50 50 1 1 B
|
||||
X VCC 8 0 800 100 D 50 50 1 1 W
|
||||
X CS0 9 -600 0 100 R 50 50 1 1 I
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# Regulator_Linear_MCP1804x-3302xOT
|
||||
#
|
||||
DEF Regulator_Linear_MCP1804x-3302xOT U 0 10 Y Y 1 F N
|
||||
F0 "U" -250 225 50 H V C CNN
|
||||
F1 "Regulator_Linear_MCP1804x-3302xOT" 0 225 50 H V L CNN
|
||||
F2 "Package_TO_SOT_SMD:SOT-23-5" 0 300 50 H I C CNN
|
||||
F3 "" 0 0 50 H I C CNN
|
||||
ALIAS MCP1804x-2502xOT MCP1804x-3002xOT MCP1804x-3302xOT MCP1804x-5002xOT MCP1804x-A002xOT MCP1804x-C002xOT
|
||||
$FPLIST
|
||||
SOT?23*
|
||||
$ENDFPLIST
|
||||
DRAW
|
||||
S -300 -200 300 175 0 1 10 f
|
||||
X VIN 1 -400 100 100 R 50 50 1 1 W
|
||||
X GND 2 0 -300 100 U 50 50 1 1 W
|
||||
X NC 3 300 0 100 L 50 50 1 1 N N
|
||||
X ~SHDN 4 -400 0 100 R 50 50 1 1 I
|
||||
X VOUT 5 400 100 100 L 50 50 1 1 w
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# Switch_SW_Push
|
||||
#
|
||||
DEF Switch_SW_Push SW 0 40 N N 1 F N
|
||||
F0 "SW" 50 100 50 H V L CNN
|
||||
F1 "Switch_SW_Push" 0 -60 50 H V C CNN
|
||||
F2 "" 0 200 50 H I C CNN
|
||||
F3 "" 0 200 50 H I C CNN
|
||||
DRAW
|
||||
C -80 0 20 0 1 0 N
|
||||
C 80 0 20 0 1 0 N
|
||||
P 2 0 1 0 0 50 0 120 N
|
||||
P 2 0 1 0 100 50 -100 50 N
|
||||
X 1 1 -200 0 100 R 50 50 0 1 P
|
||||
X 2 2 200 0 100 L 50 50 0 1 P
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# power_+BATT
|
||||
#
|
||||
DEF power_+BATT #PWR 0 0 Y Y 1 F P
|
||||
F0 "#PWR" 0 -150 50 H I C CNN
|
||||
F1 "power_+BATT" 0 140 50 H V C CNN
|
||||
F2 "" 0 0 50 H I C CNN
|
||||
F3 "" 0 0 50 H I C CNN
|
||||
DRAW
|
||||
P 2 0 1 0 -30 50 0 100 N
|
||||
P 2 0 1 0 0 0 0 100 N
|
||||
P 2 0 1 0 0 100 30 50 N
|
||||
X +BATT 1 0 0 0 U 50 50 1 1 W N
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# power_GND
|
||||
#
|
||||
DEF power_GND #PWR 0 0 Y Y 1 F P
|
||||
F0 "#PWR" 0 -250 50 H I C CNN
|
||||
F1 "power_GND" 0 -150 50 H V C CNN
|
||||
F2 "" 0 0 50 H I C CNN
|
||||
F3 "" 0 0 50 H I C CNN
|
||||
DRAW
|
||||
P 6 0 1 0 0 0 0 -50 50 -50 0 -100 -50 -50 0 -50 N
|
||||
X GND 1 0 0 0 D 50 50 1 1 W N
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
# power_VCC
|
||||
#
|
||||
DEF power_VCC #PWR 0 0 Y Y 1 F P
|
||||
F0 "#PWR" 0 -150 50 H I C CNN
|
||||
F1 "power_VCC" 0 150 50 H V C CNN
|
||||
F2 "" 0 0 50 H I C CNN
|
||||
F3 "" 0 0 50 H I C CNN
|
||||
DRAW
|
||||
C 0 75 25 0 1 0 N
|
||||
P 2 0 1 0 0 0 0 50 N
|
||||
X VCC 1 0 0 0 U 50 50 1 1 W N
|
||||
ENDDRAW
|
||||
ENDDEF
|
||||
#
|
||||
#End Library
|
||||
1325
ecad/IotButtonV0/IotButtonV0.bak
Normal file
1325
ecad/IotButtonV0/IotButtonV0.bak
Normal file
File diff suppressed because it is too large
Load Diff
1
ecad/IotButtonV0/IotButtonV0.kicad_pcb
Normal file
1
ecad/IotButtonV0/IotButtonV0.kicad_pcb
Normal file
@@ -0,0 +1 @@
|
||||
(kicad_pcb (version 4) (host kicad "dummy file") )
|
||||
33
ecad/IotButtonV0/IotButtonV0.pro
Normal file
33
ecad/IotButtonV0/IotButtonV0.pro
Normal file
@@ -0,0 +1,33 @@
|
||||
update=22/05/2015 07:44:53
|
||||
version=1
|
||||
last_client=kicad
|
||||
[general]
|
||||
version=1
|
||||
RootSch=
|
||||
BoardNm=
|
||||
[pcbnew]
|
||||
version=1
|
||||
LastNetListRead=
|
||||
UseCmpFile=1
|
||||
PadDrill=0.600000000000
|
||||
PadDrillOvalY=0.600000000000
|
||||
PadSizeH=1.500000000000
|
||||
PadSizeV=1.500000000000
|
||||
PcbTextSizeV=1.500000000000
|
||||
PcbTextSizeH=1.500000000000
|
||||
PcbTextThickness=0.300000000000
|
||||
ModuleTextSizeV=1.000000000000
|
||||
ModuleTextSizeH=1.000000000000
|
||||
ModuleTextSizeThickness=0.150000000000
|
||||
SolderMaskClearance=0.000000000000
|
||||
SolderMaskMinWidth=0.000000000000
|
||||
DrawSegmentWidth=0.200000000000
|
||||
BoardOutlineThickness=0.100000000000
|
||||
ModuleOutlineThickness=0.150000000000
|
||||
[cvpcb]
|
||||
version=1
|
||||
NetIExt=net
|
||||
[eeschema]
|
||||
version=1
|
||||
LibDir=
|
||||
[eeschema/libraries]
|
||||
1354
ecad/IotButtonV0/IotButtonV0.sch
Normal file
1354
ecad/IotButtonV0/IotButtonV0.sch
Normal file
File diff suppressed because it is too large
Load Diff
33
ecad/IotButtonV0/_saved_IotButtonV0.pro
Normal file
33
ecad/IotButtonV0/_saved_IotButtonV0.pro
Normal file
@@ -0,0 +1,33 @@
|
||||
update=22/05/2015 07:44:53
|
||||
version=1
|
||||
last_client=kicad
|
||||
[general]
|
||||
version=1
|
||||
RootSch=
|
||||
BoardNm=
|
||||
[pcbnew]
|
||||
version=1
|
||||
LastNetListRead=
|
||||
UseCmpFile=1
|
||||
PadDrill=0.600000000000
|
||||
PadDrillOvalY=0.600000000000
|
||||
PadSizeH=1.500000000000
|
||||
PadSizeV=1.500000000000
|
||||
PcbTextSizeV=1.500000000000
|
||||
PcbTextSizeH=1.500000000000
|
||||
PcbTextThickness=0.300000000000
|
||||
ModuleTextSizeV=1.000000000000
|
||||
ModuleTextSizeH=1.000000000000
|
||||
ModuleTextSizeThickness=0.150000000000
|
||||
SolderMaskClearance=0.000000000000
|
||||
SolderMaskMinWidth=0.000000000000
|
||||
DrawSegmentWidth=0.200000000000
|
||||
BoardOutlineThickness=0.100000000000
|
||||
ModuleOutlineThickness=0.150000000000
|
||||
[cvpcb]
|
||||
version=1
|
||||
NetIExt=net
|
||||
[eeschema]
|
||||
version=1
|
||||
LibDir=
|
||||
[eeschema/libraries]
|
||||
@@ -16,7 +16,7 @@ platform = atmelavr
|
||||
framework = arduino
|
||||
board = attiny85
|
||||
upload_protocol = custom
|
||||
upload_port = /dev/ttyACM1
|
||||
upload_port = /dev/ttyACM0
|
||||
upload_speed = 19200
|
||||
upload_flags =
|
||||
-C
|
||||
|
||||
@@ -36,7 +36,6 @@ void AttinyDebounce::update(unsigned long millisNow) {
|
||||
|
||||
break;
|
||||
case TRIGGERED:
|
||||
// TODO: Debounce this part too
|
||||
if (!assertedNow) {
|
||||
state = IDLE;
|
||||
}
|
||||
|
||||
@@ -29,13 +29,14 @@ constexpr uint8_t BUTTON_PIN = 1; // Button press
|
||||
constexpr uint8_t RTC_PIN = 4; // RTC alarm (currently, from the ESP8266 RTC)
|
||||
constexpr uint8_t AUX_PIN = 5; // Another wakeup source. May be used for other sensors in the future.
|
||||
|
||||
constexpr uint8_t DEBUG_LED = RTC_PIN;
|
||||
// constexpr uint8_t DEBUG_LED = RTC_PIN;
|
||||
|
||||
constexpr unsigned long CMD_TIMEOUT_MILLIS = 1000; // After this period of with no commands, the device will sleep
|
||||
constexpr unsigned long WAKE_PULSE_MILLIS = 100; // Length of pulse sent to wake the main MCU
|
||||
constexpr unsigned long LONG_PRESS_MILLIS = 5000;
|
||||
constexpr unsigned long I2C_WATCHDOG_PERIOD_MILLIS = 2000; // If we don't get a request from the Main MCU in this time, just go to sleep.
|
||||
constexpr unsigned long DEBOUNCE_MILLIS = 100;
|
||||
constexpr unsigned long I2C_WATCHDOG_PERIOD_MILLIS = 1000; // If we don't get a request from the Main MCU in this time, just go to sleep.
|
||||
constexpr unsigned long DEBOUNCE_MILLIS = 1;
|
||||
constexpr unsigned long NO_WAKEUP_SRC_TIMEOUT_MILLIS = DEBOUNCE_MILLIS + 5;
|
||||
|
||||
constexpr uint8_t I2C_DEVICE_ADDR = 0x4F; // Chosen arbitrarily
|
||||
|
||||
@@ -78,6 +79,7 @@ static unsigned long startWakeMillis, i2cWatchDogTime;
|
||||
static I2cReq i2c_cmd_byte;
|
||||
static volatile WakeSource wakeSource;
|
||||
static volatile unsigned long endButtonMillis;
|
||||
static unsigned long wakeupMillis = 0;
|
||||
|
||||
static void i2cReceiveHook(int numBytes) {
|
||||
while (numBytes > 0) {
|
||||
@@ -96,8 +98,6 @@ static void i2cRequestHook() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
static void enablePinChangeInterrupt() {
|
||||
GIMSK |= _BV(PCIE);
|
||||
PCMSK |= _BV(BUTTON_PIN);
|
||||
@@ -123,15 +123,15 @@ static void resetState() {
|
||||
void setup() {
|
||||
// Init pin states
|
||||
pinMode(MAIN_MCU_NRESET_PIN, OUTPUT);
|
||||
pinMode(BUTTON_PIN, INPUT);
|
||||
pinMode(RTC_PIN, INPUT);
|
||||
pinMode(AUX_PIN, INPUT);
|
||||
pinMode(DEBUG_LED, OUTPUT);
|
||||
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
||||
pinMode(RTC_PIN, INPUT_PULLUP);
|
||||
pinMode(AUX_PIN, INPUT_PULLUP);
|
||||
// pinMode(DEBUG_LED, OUTPUT);
|
||||
|
||||
// On first run, reset the main MCU to get in sync.
|
||||
digitalWrite(MAIN_MCU_NRESET_PIN, HIGH);
|
||||
delay(WAKE_PULSE_MILLIS);
|
||||
digitalWrite(MAIN_MCU_NRESET_PIN, LOW);
|
||||
delay(WAKE_PULSE_MILLIS);
|
||||
digitalWrite(MAIN_MCU_NRESET_PIN, HIGH);
|
||||
|
||||
appState = AppState::SLEEP;
|
||||
|
||||
@@ -151,21 +151,19 @@ static void buttonCallback(int index) {
|
||||
}
|
||||
|
||||
ISR(PCINT0_vect) {
|
||||
unsigned long millisNow = millis();
|
||||
ButtonDebounce.update(millisNow);
|
||||
ButtonDebounce.update(millis());
|
||||
}
|
||||
|
||||
|
||||
void loop() {
|
||||
unsigned long millisNow = millis();
|
||||
ButtonDebounce.update(millisNow);
|
||||
ButtonDebounce.update(millis());
|
||||
|
||||
switch (appState) {
|
||||
case AppState::SLEEP:
|
||||
resetState();
|
||||
//digitalWrite(DEBUG_LED, HIGH);
|
||||
// digitalWrite(DEBUG_LED, LOW);
|
||||
sleepUntilPinChange();
|
||||
//digitalWrite(DEBUG_LED, LOW);
|
||||
wakeupMillis = millis();
|
||||
// digitalWrite(DEBUG_LED, HIGH);
|
||||
appState = AppState::WATCHING_INPUT;
|
||||
break;
|
||||
case AppState::WATCHING_INPUT:
|
||||
@@ -173,21 +171,24 @@ void loop() {
|
||||
// Keep waiting. For example, waiting to see if long or short button press.
|
||||
// TODO: Not yet implemented
|
||||
} else if (wakeSource == WakeSource::NONE) {
|
||||
// appState = AppState::SLEEP;
|
||||
if (millis() > wakeupMillis + NO_WAKEUP_SRC_TIMEOUT_MILLIS) {
|
||||
appState = AppState::SLEEP;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// We know the wakeup source. Time to inform the main MCU.
|
||||
appState = AppState::START_WAKE_MAIN_MCU;
|
||||
}
|
||||
break;
|
||||
case AppState::START_WAKE_MAIN_MCU:
|
||||
digitalWrite(MAIN_MCU_NRESET_PIN, HIGH);
|
||||
digitalWrite(MAIN_MCU_NRESET_PIN, LOW);
|
||||
startWakeMillis = millis();
|
||||
appState = AppState::WAKING_MAIN_MCU;
|
||||
break;
|
||||
case AppState::WAKING_MAIN_MCU:
|
||||
if (millis() > startWakeMillis + WAKE_PULSE_MILLIS) {
|
||||
appState = AppState::WAITING_FOR_I2C;
|
||||
digitalWrite(MAIN_MCU_NRESET_PIN, LOW);
|
||||
digitalWrite(MAIN_MCU_NRESET_PIN, HIGH);
|
||||
}
|
||||
break;
|
||||
case AppState::WAITING_FOR_I2C:
|
||||
|
||||
2
firmware/main_mcu/.gitignore
vendored
Normal file
2
firmware/main_mcu/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.pio
|
||||
src/config.h
|
||||
39
firmware/main_mcu/include/README
Normal file
39
firmware/main_mcu/include/README
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
This directory is intended for project header files.
|
||||
|
||||
A header file is a file containing C declarations and macro definitions
|
||||
to be shared between several project source files. You request the use of a
|
||||
header file in your project source file (C, C++, etc) located in `src` folder
|
||||
by including it, with the C preprocessing directive `#include'.
|
||||
|
||||
```src/main.c
|
||||
|
||||
#include "header.h"
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Including a header file produces the same results as copying the header file
|
||||
into each source file that needs it. Such copying would be time-consuming
|
||||
and error-prone. With a header file, the related declarations appear
|
||||
in only one place. If they need to be changed, they can be changed in one
|
||||
place, and programs that include the header file will automatically use the
|
||||
new version when next recompiled. The header file eliminates the labor of
|
||||
finding and changing all the copies as well as the risk that a failure to
|
||||
find one copy will result in inconsistencies within a program.
|
||||
|
||||
In C, the usual convention is to give header files names that end with `.h'.
|
||||
It is most portable to use only letters, digits, dashes, and underscores in
|
||||
header file names, and at most one dot.
|
||||
|
||||
Read more about using header files in official GCC documentation:
|
||||
|
||||
* Include Syntax
|
||||
* Include Operation
|
||||
* Once-Only Headers
|
||||
* Computed Includes
|
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||
46
firmware/main_mcu/lib/README
Normal file
46
firmware/main_mcu/lib/README
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into executable file.
|
||||
|
||||
The source code of each library should be placed in a an own separate directory
|
||||
("lib/your_library_name/[here are source files]").
|
||||
|
||||
For example, see a structure of the following two libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
and a contents of `src/main.c`:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||
26
firmware/main_mcu/platformio.ini
Normal file
26
firmware/main_mcu/platformio.ini
Normal file
@@ -0,0 +1,26 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env]
|
||||
platform = espressif8266
|
||||
framework = arduino
|
||||
board = esp12e
|
||||
lib_deps =
|
||||
knolleary/PubSubClient@^2.8.0
|
||||
bblanchon/ArduinoJson@^6.17.2
|
||||
upload_speed = 115200
|
||||
|
||||
[env:button-release]
|
||||
|
||||
[env:button-timing]
|
||||
build_flags = -D DEBUG_SKETCH_TIMING
|
||||
|
||||
[env:button-debug]
|
||||
build_flags = -D DEBUG_SKETCH
|
||||
114
firmware/main_mcu/src/main.cpp
Normal file
114
firmware/main_mcu/src/main.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
#include <Arduino.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "mqtt.h"
|
||||
#include "util.h"
|
||||
|
||||
#define BTN_PIN D0
|
||||
#define RED_LED_PIN 4
|
||||
#define GREEN_LED_PIN 5
|
||||
#define BLUE_LED_PIN 12
|
||||
|
||||
#define LONG_PRESS_MILLIS 1000
|
||||
|
||||
static WiFiClient wifiClient;
|
||||
static PubSubClient mqttClient;
|
||||
|
||||
void blink(int pin, int times, int onMillis, int offMillis) {
|
||||
// for (int i = 0; i < times; i++) {
|
||||
// digitalWrite(pin, HIGH);
|
||||
// delay(onMillis);
|
||||
// digitalWrite(pin, LOW);
|
||||
// delay(offMillis);
|
||||
// }
|
||||
}
|
||||
|
||||
void fail(const char *message) {
|
||||
PRINT(message);
|
||||
blink(RED_LED_PIN, 3, 50, 50);
|
||||
// TODO: go back to sleep
|
||||
}
|
||||
|
||||
void success() {
|
||||
blink(GREEN_LED_PIN, 1, 50, 0);
|
||||
}
|
||||
|
||||
static void initConnection() {
|
||||
initWifi(wifiClient);
|
||||
initMqtt(mqttClient, wifiClient);
|
||||
}
|
||||
|
||||
void handleShortButtonWakeup() {
|
||||
initConnection();
|
||||
|
||||
if (!publishTriggerMessage(mqttClient, String("short_press"))) {
|
||||
fail("failed to publish trigger message\n");
|
||||
}
|
||||
|
||||
if (!publishBatteryMessage(mqttClient, 100)) {
|
||||
fail("Failed to publish battery message\n");
|
||||
}
|
||||
}
|
||||
|
||||
void handleLongButtonWakeup() {
|
||||
initConnection();
|
||||
if (!publishHABatteryDiscoveryConfig(mqttClient)) {
|
||||
fail("Failed to publish battery config\n");
|
||||
}
|
||||
if (!publishHATriggerDiscoveryConfig(mqttClient)) {
|
||||
fail("Failed to publish trigger config\n");
|
||||
}
|
||||
if (!publishBatteryMessage(mqttClient, 100)) {
|
||||
fail("Failed to publish trigger message\n");
|
||||
}
|
||||
|
||||
success();
|
||||
}
|
||||
|
||||
void setup() {
|
||||
// Sample the input button ASAP.
|
||||
// pinMode(BTN_PIN, INPUT);
|
||||
// bool btn_value = digitalRead(BTN_PIN);
|
||||
|
||||
unsigned long startTimingMillis = millis();
|
||||
|
||||
pinMode(RED_LED_PIN, OUTPUT);
|
||||
pinMode(BLUE_LED_PIN, OUTPUT);
|
||||
pinMode(GREEN_LED_PIN, OUTPUT);
|
||||
|
||||
#if defined(DEBUG_SKETCH) || defined(DEBUG_SKETCH_TIMING)
|
||||
Serial.begin(9600);
|
||||
#endif
|
||||
|
||||
PRINTLN("\nBeginning setup\n");
|
||||
PRINT("Device name:");
|
||||
PRINTLN(getDeviceHumanName());
|
||||
|
||||
handleShortButtonWakeup();
|
||||
|
||||
digitalWrite(RED_LED_PIN, LOW);
|
||||
digitalWrite(GREEN_LED_PIN, LOW);
|
||||
digitalWrite(BLUE_LED_PIN, LOW);
|
||||
|
||||
// We've done our work- back to sleep.
|
||||
PRINT("Going to sleep...\n");
|
||||
#if defined(DEBUG_SKETCH_TIMING)
|
||||
unsigned long endTimeMillis = millis();
|
||||
Serial.print("Ran for " + String(endTimeMillis - startTimingMillis) + " milliseconds\n");
|
||||
#endif
|
||||
|
||||
// Drain all of our pending requests
|
||||
mqttClient.disconnect();
|
||||
wifiClient.flush();
|
||||
while (mqttClient.state() != -1) {
|
||||
yield();
|
||||
}
|
||||
ESP.deepSleep(0, RFMode::RF_NO_CAL);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
|
||||
}
|
||||
146
firmware/main_mcu/src/mqtt.cpp
Normal file
146
firmware/main_mcu/src/mqtt.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "mqtt.h"
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "util.h"
|
||||
|
||||
#define MAX_PACKET_SIZE 1024
|
||||
#define TIMEOUT_MILLIS 1000
|
||||
#define TRIGGER_KEY "event"
|
||||
|
||||
#define VALUE_TEMPLATE(key) ("{{ value_json." key " }}")
|
||||
|
||||
// TODO: This should really be a class, this is kinda busted
|
||||
static bool isDone = true;
|
||||
|
||||
void initMqtt(PubSubClient &mqttClient, WiFiClient &wifiClient) {
|
||||
mqttClient.setServer(CONFIG_MQTT_SERVER, CONFIG_MQTT_SERVER_PORT);
|
||||
mqttClient.setClient(wifiClient);
|
||||
mqttClient.setBufferSize(MAX_PACKET_SIZE);
|
||||
mqttClient.setCallback([](char *, unsigned char*, unsigned int){
|
||||
PRINTLN("Message sent!");
|
||||
isDone = true;
|
||||
});
|
||||
|
||||
PRINT("Connecting to MQTT broker...");
|
||||
while (!mqttClient.connected()) {
|
||||
PRINT(".");
|
||||
mqttClient.connect(getDeviceMachineName().c_str());
|
||||
}
|
||||
PRINTLN("\nConnected!");
|
||||
}
|
||||
|
||||
static bool waitTilDone(PubSubClient &mqttClient) {
|
||||
mqttClient.loop();
|
||||
return true;
|
||||
// unsigned long startTime = millis();
|
||||
|
||||
// do {
|
||||
// mqttClient.loop();
|
||||
// } while (!isDone && startTime + TIMEOUT_MILLIS > millis());
|
||||
|
||||
// return isDone;
|
||||
}
|
||||
|
||||
static String getHATriggerConfigTopic() {
|
||||
return String(CONFIG_HOMEASSISTANT_MQTT_PREFIX "device_automation/") + getDeviceMachineName() + "/config";
|
||||
}
|
||||
|
||||
static String getHASensorConfigTopic() {
|
||||
return String(CONFIG_HOMEASSISTANT_MQTT_PREFIX "sensor/") + getDeviceMachineName() + "/config";
|
||||
}
|
||||
|
||||
|
||||
static String getButtonTopicBase(bool abbrev) {
|
||||
if (abbrev) {
|
||||
return String("~");
|
||||
} else {
|
||||
return String(CONFIG_BUTTON_EVENT_TOPIC_PREFIX) + getDeviceMachineName();
|
||||
}
|
||||
}
|
||||
|
||||
static String getButtonStateTopic(bool abbrev) {
|
||||
return getButtonTopicBase(abbrev) + "/" CONFIG_BUTTON_STATE_TOPIC_NAME;
|
||||
}
|
||||
|
||||
static String getButtonTriggerTopic(bool abbrev) {
|
||||
return getButtonTopicBase(abbrev) + "/" CONFIG_BUTTON_TRIGGER_TOPIC_NAME;
|
||||
}
|
||||
|
||||
static bool publishJsonDocument(PubSubClient &mqttClient, String topic, JsonDocument &message, bool retain) {
|
||||
char str[MAX_PACKET_SIZE];
|
||||
size_t len = serializeJson(message, str, MAX_PACKET_SIZE);
|
||||
PRINT(topic);
|
||||
PRINT(":");
|
||||
PRINTLN(str);
|
||||
isDone = false;
|
||||
if (!mqttClient.publish(topic.c_str(), (uint8_t *) str, len, retain)) {
|
||||
return false;
|
||||
}
|
||||
return waitTilDone(mqttClient);
|
||||
}
|
||||
|
||||
static void addDeviceJson(JsonDocument &doc) {
|
||||
JsonObject device = doc.createNestedObject("device");
|
||||
device["name"] = getDeviceHumanName();
|
||||
device["mf"] = "DIY"; // manufacturer
|
||||
device["mdl"] = "IoT Button - ESP8266"; // model
|
||||
device["sw"] = "1.0"; // sw_version
|
||||
|
||||
JsonArray ids = device.createNestedArray("ids");
|
||||
ids.add(getDeviceId());
|
||||
}
|
||||
|
||||
bool publishBatteryMessage(PubSubClient &mqttClient, int percentage) {
|
||||
StaticJsonDocument<MAX_PACKET_SIZE> json;
|
||||
json["battery"] = percentage;
|
||||
|
||||
isDone = false;
|
||||
PRINTLN("Publishing battery message");
|
||||
return publishJsonDocument(mqttClient, getButtonStateTopic(false), json, true);
|
||||
}
|
||||
|
||||
bool publishTriggerMessage(PubSubClient &mqttClient, String message) {
|
||||
isDone = false;
|
||||
PRINTLN("Publishing trigger message");
|
||||
if (!mqttClient.publish(getButtonTriggerTopic(false).c_str(), message.c_str(), false)) {
|
||||
return false;
|
||||
}
|
||||
return waitTilDone(mqttClient);
|
||||
}
|
||||
|
||||
bool publishHABatteryDiscoveryConfig(PubSubClient &mqttClient) {
|
||||
StaticJsonDocument<MAX_PACKET_SIZE> json;
|
||||
bool abbrev = true;
|
||||
|
||||
json["~"] = getButtonTopicBase(false);
|
||||
json["stat_t"] = getButtonStateTopic(abbrev); // state_topic
|
||||
json["val_tpl"] = VALUE_TEMPLATE("battery"); // state_value_template
|
||||
json["uniq_id"] = getDeviceMachineName() + "-battery"; // unique_id
|
||||
json["dev_cla"] = "battery"; // device_class
|
||||
json["name"] = getDeviceHumanName() + ": Battery";
|
||||
|
||||
addDeviceJson(json);
|
||||
|
||||
PRINTLN("Publishing device sensor config");
|
||||
return publishJsonDocument(mqttClient, getHASensorConfigTopic(), json, true);
|
||||
}
|
||||
|
||||
bool publishHATriggerDiscoveryConfig(PubSubClient &mqttClient) {
|
||||
StaticJsonDocument<MAX_PACKET_SIZE> json;
|
||||
bool abbrev = true;
|
||||
|
||||
// json["name"] = getDeviceHumanName();
|
||||
json["atype"] = "trigger"; // automation_type
|
||||
json["type"] = "button_short_press";
|
||||
json["stype"] = "button_1"; // subtype
|
||||
|
||||
json["~"] = getButtonTopicBase(false);
|
||||
json["t"] = getButtonTriggerTopic(abbrev); // topic
|
||||
|
||||
addDeviceJson(json);
|
||||
|
||||
PRINTLN("Publishing device trigger discovery");
|
||||
return publishJsonDocument(mqttClient, getHATriggerConfigTopic(), json, true);
|
||||
}
|
||||
16
firmware/main_mcu/src/mqtt.h
Normal file
16
firmware/main_mcu/src/mqtt.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef __MQTT_H_
|
||||
#define __MQTT_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
|
||||
void initMqtt(PubSubClient& client, WiFiClient &wifiClient);
|
||||
|
||||
bool publishHABatteryDiscoveryConfig(PubSubClient &client);
|
||||
bool publishHATriggerDiscoveryConfig(PubSubClient &client);
|
||||
|
||||
bool publishTriggerMessage(PubSubClient &client, String message);
|
||||
bool publishBatteryMessage(PubSubClient &client, int percent);
|
||||
|
||||
#endif // __MQTT_H_
|
||||
41
firmware/main_mcu/src/util.cpp
Normal file
41
firmware/main_mcu/src/util.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "util.h"
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#define DEVICE_NAME_PREFIX "button-"
|
||||
|
||||
#define PRINT_DELAY_MILLIS 500
|
||||
|
||||
String getDeviceId() {
|
||||
return String(ESP.getChipId(), HEX);
|
||||
}
|
||||
|
||||
String getDeviceMachineName() {
|
||||
return String(DEVICE_NAME_PREFIX) + getDeviceId();
|
||||
}
|
||||
|
||||
String getDeviceHumanName() {
|
||||
return "IoT Button (" + getDeviceId() + ")";
|
||||
}
|
||||
|
||||
void initWifi(WiFiClient &wifi) {
|
||||
PRINT("Connecting to WiFi using MAC: ");
|
||||
PRINTLN(WiFi.macAddress());
|
||||
|
||||
WiFi.hostname(getDeviceMachineName().c_str());
|
||||
WiFi.begin(CONFIG_SSID, CONFIG_PSK);
|
||||
long last_time = millis();
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
yield();
|
||||
long time = millis();
|
||||
if (time >= last_time + PRINT_DELAY_MILLIS) {
|
||||
PRINT(".");
|
||||
last_time = time;
|
||||
}
|
||||
}
|
||||
|
||||
PRINTLN("\nConnected!");
|
||||
PRINT("IP Address: ");
|
||||
PRINTLN(WiFi.localIP());
|
||||
|
||||
}
|
||||
21
firmware/main_mcu/src/util.h
Normal file
21
firmware/main_mcu/src/util.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef __UTIL_H_
|
||||
#define __UTIL_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
|
||||
#ifdef DEBUG_SKETCH
|
||||
#define PRINT(x) { Serial.print(x); } while (0)
|
||||
#define PRINTLN(x) { Serial.println(x); } while (0)
|
||||
#else
|
||||
#define PRINT(x)
|
||||
#define PRINTLN(x)
|
||||
#endif
|
||||
|
||||
String getDeviceHumanName();
|
||||
String getDeviceMachineName();
|
||||
String getDeviceId();
|
||||
void initWifi(WiFiClient &wifi);
|
||||
|
||||
|
||||
#endif // __UTIL_H_
|
||||
11
firmware/main_mcu/test/README
Normal file
11
firmware/main_mcu/test/README
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
This directory is intended for PlatformIO Unit Testing and project tests.
|
||||
|
||||
Unit Testing is a software testing method by which individual units of
|
||||
source code, sets of one or more MCU program modules together with associated
|
||||
control data, usage procedures, and operating procedures, are tested to
|
||||
determine whether they are fit for use. Unit testing finds problems early
|
||||
in the development cycle.
|
||||
|
||||
More information about PlatformIO Unit Testing:
|
||||
- https://docs.platformio.org/page/plus/unit-testing.html
|
||||
Reference in New Issue
Block a user