Merge branch 'manualValidation' of JF/PineTime into develop
This commit is contained in:
commit
18686ac2cb
|
@ -114,3 +114,24 @@ sudo dfu.py -z /home/jf/nrf52/bootloader/dfu.zip -a <pinetime MAC address> --leg
|
||||||
`
|
`
|
||||||
|
|
||||||
**Note** : dfu.py is a slightly modified version of [this repo](https://github.com/daniel-thompson/ota-dfu-python).
|
**Note** : dfu.py is a slightly modified version of [this repo](https://github.com/daniel-thompson/ota-dfu-python).
|
||||||
|
|
||||||
|
See [this page](../doc/CompanionApps/NrfconnectOTA.md) for more info about OTA with NRFConect
|
||||||
|
|
||||||
|
### Firmware validation
|
||||||
|
Once the OTA is done, InfiniTime will reset the watch to apply the update. When the watch reboots, the new firmware is running.
|
||||||
|
|
||||||
|
One last step is needed to finalize the upgrade : the new firmware must be manually validated. If the watch resets while the image is not validated, the bootloader will automatically revert to the previous version of the firmware.
|
||||||
|
|
||||||
|
If the new firmware is working correctly, open the application menu and tap on the 'check' app. This apps displays the version of the firmware that is currently running, and allows you to validate the firmware, or reset the device to rollback to the previous version.
|
||||||
|
|
||||||
|
Firmware validation application in the menu:
|
||||||
|
|
||||||
|
![Firmware Validation App](../doc/CompanionApps/firmwareValidationApp.jpg "Firmware Validation App")
|
||||||
|
|
||||||
|
The firmware is not validated yet. Tap 'Validate' to validate it, or 'Reset' to rollback to the previous version.
|
||||||
|
|
||||||
|
![Firmware Not Validated](../doc/CompanionApps/firmwareNoValidated.jpg "Firmware Not Validated")
|
||||||
|
|
||||||
|
The firmware is validated!
|
||||||
|
|
||||||
|
![Firmware Validated](../doc/CompanionApps/firmwareValidated.jpg "Firmware Validated")
|
||||||
|
|
BIN
doc/CompanionApps/firmwareNoValidated.jpg
Normal file
BIN
doc/CompanionApps/firmwareNoValidated.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 187 KiB |
BIN
doc/CompanionApps/firmwareValidated.jpg
Normal file
BIN
doc/CompanionApps/firmwareValidated.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 208 KiB |
BIN
doc/CompanionApps/firmwareValidationApp.jpg
Normal file
BIN
doc/CompanionApps/firmwareValidationApp.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 176 KiB |
|
@ -337,6 +337,7 @@ list(APPEND SOURCE_FILES
|
||||||
DisplayApp/Screens/Label.cpp
|
DisplayApp/Screens/Label.cpp
|
||||||
DisplayApp/Screens/FirmwareUpdate.cpp
|
DisplayApp/Screens/FirmwareUpdate.cpp
|
||||||
DisplayApp/Screens/Music.cpp
|
DisplayApp/Screens/Music.cpp
|
||||||
|
DisplayApp/Screens/FirmwareValidation.cpp
|
||||||
DisplayApp/Screens/ApplicationList.cpp
|
DisplayApp/Screens/ApplicationList.cpp
|
||||||
main.cpp
|
main.cpp
|
||||||
drivers/St7789.cpp
|
drivers/St7789.cpp
|
||||||
|
@ -359,6 +360,7 @@ list(APPEND SOURCE_FILES
|
||||||
Components/Ble/CurrentTimeService.cpp
|
Components/Ble/CurrentTimeService.cpp
|
||||||
Components/Ble/AlertNotificationService.cpp
|
Components/Ble/AlertNotificationService.cpp
|
||||||
Components/Ble/MusicService.cpp
|
Components/Ble/MusicService.cpp
|
||||||
|
Components/FirmwareValidator/FirmwareValidator.cpp
|
||||||
drivers/Cst816s.cpp
|
drivers/Cst816s.cpp
|
||||||
FreeRTOS/port.c
|
FreeRTOS/port.c
|
||||||
FreeRTOS/port_cmsis_systick.c
|
FreeRTOS/port_cmsis_systick.c
|
||||||
|
@ -417,6 +419,7 @@ set(INCLUDE_FILES
|
||||||
DisplayApp/Screens/ScreenList.h
|
DisplayApp/Screens/ScreenList.h
|
||||||
DisplayApp/Screens/Label.h
|
DisplayApp/Screens/Label.h
|
||||||
DisplayApp/Screens/FirmwareUpdate.h
|
DisplayApp/Screens/FirmwareUpdate.h
|
||||||
|
DisplayApp/Screens/FirmwareValidation.h
|
||||||
DisplayApp/Screens/ApplicationList.h
|
DisplayApp/Screens/ApplicationList.h
|
||||||
DisplayApp/Apps.h
|
DisplayApp/Apps.h
|
||||||
drivers/St7789.h
|
drivers/St7789.h
|
||||||
|
@ -436,7 +439,8 @@ set(INCLUDE_FILES
|
||||||
Components/Ble/CurrentTimeClient.h
|
Components/Ble/CurrentTimeClient.h
|
||||||
Components/Ble/AlertNotificationClient.h
|
Components/Ble/AlertNotificationClient.h
|
||||||
Components/Ble/DfuService.h
|
Components/Ble/DfuService.h
|
||||||
drivers/Cst816s.h
|
Components/FirmwareValidator/FirmwareValidator.h
|
||||||
|
drivers/Cst816s.h
|
||||||
FreeRTOS/portmacro.h
|
FreeRTOS/portmacro.h
|
||||||
FreeRTOS/portmacro_cmsis.h
|
FreeRTOS/portmacro_cmsis.h
|
||||||
libs/date/includes/date/tz.h
|
libs/date/includes/date/tz.h
|
||||||
|
|
20
src/Components/FirmwareValidator/FirmwareValidator.cpp
Normal file
20
src/Components/FirmwareValidator/FirmwareValidator.cpp
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#include <drivers/InternalFlash.h>
|
||||||
|
#include <hal/nrf_rtc.h>
|
||||||
|
|
||||||
|
#include "FirmwareValidator.h"
|
||||||
|
|
||||||
|
using namespace Pinetime::Controllers;
|
||||||
|
|
||||||
|
bool FirmwareValidator::IsValidated() const {
|
||||||
|
auto* imageOkPtr = reinterpret_cast<uint32_t *>(validBitAdress);
|
||||||
|
return (*imageOkPtr) == validBitValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FirmwareValidator::Validate() {
|
||||||
|
if(!IsValidated())
|
||||||
|
Pinetime::Drivers::InternalFlash::WriteWord(validBitAdress, validBitValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FirmwareValidator::Reset() {
|
||||||
|
NVIC_SystemReset();
|
||||||
|
}
|
18
src/Components/FirmwareValidator/FirmwareValidator.h
Normal file
18
src/Components/FirmwareValidator/FirmwareValidator.h
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace Pinetime {
|
||||||
|
namespace Controllers {
|
||||||
|
class FirmwareValidator {
|
||||||
|
public:
|
||||||
|
void Validate();
|
||||||
|
bool IsValidated() const;
|
||||||
|
|
||||||
|
void Reset();
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t validBitAdress {0x7BFE8};
|
||||||
|
static constexpr uint32_t validBitValue {1};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
namespace Pinetime {
|
namespace Pinetime {
|
||||||
namespace Applications {
|
namespace Applications {
|
||||||
enum class Apps {None, Launcher, Clock, SysInfo, Meter, Gauge, Brightness, Music};
|
enum class Apps {None, Launcher, Clock, SysInfo, Meter, Gauge, Brightness, Music, FirmwareValidation};
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
#include <Components/Ble/NotificationManager.h>
|
#include <Components/Ble/NotificationManager.h>
|
||||||
#include <DisplayApp/Screens/FirmwareUpdate.h>
|
#include <DisplayApp/Screens/FirmwareUpdate.h>
|
||||||
#include <DisplayApp/Screens/ApplicationList.h>
|
#include <DisplayApp/Screens/ApplicationList.h>
|
||||||
|
#include <DisplayApp/Screens/FirmwareValidation.h>
|
||||||
#include "../SystemTask/SystemTask.h"
|
#include "../SystemTask/SystemTask.h"
|
||||||
|
|
||||||
using namespace Pinetime::Applications;
|
using namespace Pinetime::Applications;
|
||||||
|
@ -189,6 +190,7 @@ void DisplayApp::RunningState() {
|
||||||
case Apps::Gauge: currentScreen.reset(new Screens::Gauge(this)); break;
|
case Apps::Gauge: currentScreen.reset(new Screens::Gauge(this)); break;
|
||||||
case Apps::Brightness : currentScreen.reset(new Screens::Brightness(this, brightnessController)); break;
|
case Apps::Brightness : currentScreen.reset(new Screens::Brightness(this, brightnessController)); break;
|
||||||
case Apps::Music : currentScreen.reset(new Screens::Music(this, systemTask.nimble().music())); break;
|
case Apps::Music : currentScreen.reset(new Screens::Music(this, systemTask.nimble().music())); break;
|
||||||
|
case Apps::FirmwareValidation: currentScreen.reset(new Screens::FirmwareValidation(this, validator)); break;
|
||||||
}
|
}
|
||||||
nextApp = Apps::None;
|
nextApp = Apps::None;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
#include <drivers/Watchdog.h>
|
#include <drivers/Watchdog.h>
|
||||||
#include <DisplayApp/Screens/Modal.h>
|
#include <DisplayApp/Screens/Modal.h>
|
||||||
#include <Components/Ble/NotificationManager.h>
|
#include <Components/Ble/NotificationManager.h>
|
||||||
|
#include <Components/FirmwareValidator/FirmwareValidator.h>
|
||||||
#include "TouchEvents.h"
|
#include "TouchEvents.h"
|
||||||
#include "Apps.h"
|
#include "Apps.h"
|
||||||
|
|
||||||
|
@ -80,6 +81,7 @@ namespace Pinetime {
|
||||||
Controllers::BrightnessController brightnessController;
|
Controllers::BrightnessController brightnessController;
|
||||||
std::unique_ptr<Screens::Modal> modal;
|
std::unique_ptr<Screens::Modal> modal;
|
||||||
Pinetime::Controllers::NotificationManager& notificationManager;
|
Pinetime::Controllers::NotificationManager& notificationManager;
|
||||||
|
Pinetime::Controllers::FirmwareValidator validator;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ ApplicationList::ApplicationList(Pinetime::Applications::DisplayApp *app) :
|
||||||
Screen(app),
|
Screen(app),
|
||||||
screens{app, {
|
screens{app, {
|
||||||
[this]() -> std::unique_ptr<Screen> { return CreateScreen1(); },
|
[this]() -> std::unique_ptr<Screen> { return CreateScreen1(); },
|
||||||
//[this]() -> std::unique_ptr<Screen> { return CreateScreen2(); },
|
[this]() -> std::unique_ptr<Screen> { return CreateScreen2(); },
|
||||||
//[this]() -> std::unique_ptr<Screen> { return CreateScreen3(); }
|
//[this]() -> std::unique_ptr<Screen> { return CreateScreen3(); }
|
||||||
}
|
}
|
||||||
} {}
|
} {}
|
||||||
|
@ -39,13 +39,15 @@ bool ApplicationList::OnTouchEvent(Pinetime::Applications::TouchEvents event) {
|
||||||
|
|
||||||
std::unique_ptr<Screen> ApplicationList::CreateScreen1() {
|
std::unique_ptr<Screen> ApplicationList::CreateScreen1() {
|
||||||
std::array<Screens::Tile::Applications, 6> applications {
|
std::array<Screens::Tile::Applications, 6> applications {
|
||||||
{{Symbols::asterisk, Apps::Meter},
|
{{Symbols::clock, Apps::Clock},
|
||||||
{Symbols::tachometer, Apps::Gauge},
|
{Symbols::music, Apps::Music},
|
||||||
{Symbols::clock, Apps::Clock},
|
{Symbols::sun, Apps::Brightness},
|
||||||
{Symbols::music, Apps::Music},
|
{Symbols::list, Apps::SysInfo},
|
||||||
{Symbols::list, Apps::SysInfo},
|
{Symbols::check, Apps::FirmwareValidation},
|
||||||
{Symbols::sun, Apps::Brightness}
|
{Symbols::none, Apps::None}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return std::unique_ptr<Screen>(new Screens::Tile(app, applications));
|
return std::unique_ptr<Screen>(new Screens::Tile(app, applications));
|
||||||
|
@ -53,12 +55,12 @@ std::unique_ptr<Screen> ApplicationList::CreateScreen1() {
|
||||||
|
|
||||||
std::unique_ptr<Screen> ApplicationList::CreateScreen2() {
|
std::unique_ptr<Screen> ApplicationList::CreateScreen2() {
|
||||||
std::array<Screens::Tile::Applications, 6> applications {
|
std::array<Screens::Tile::Applications, 6> applications {
|
||||||
{{"0", Apps::Meter},
|
{{Symbols::tachometer, Apps::Gauge},
|
||||||
{"1", Apps::Gauge},
|
{Symbols::asterisk, Apps::Meter},
|
||||||
{"2", Apps::Clock},
|
{Symbols::none, Apps::None},
|
||||||
{"3", Apps::Music},
|
{Symbols::none, Apps::None},
|
||||||
{"4", Apps::SysInfo},
|
{Symbols::none, Apps::None},
|
||||||
{"5", Apps::Brightness}
|
{Symbols::none, Apps::None}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ namespace Pinetime {
|
||||||
private:
|
private:
|
||||||
bool running = true;
|
bool running = true;
|
||||||
|
|
||||||
ScreenList<1> screens;
|
ScreenList<2> screens;
|
||||||
std::unique_ptr<Screen> CreateScreen1();
|
std::unique_ptr<Screen> CreateScreen1();
|
||||||
std::unique_ptr<Screen> CreateScreen2();
|
std::unique_ptr<Screen> CreateScreen2();
|
||||||
std::unique_ptr<Screen> CreateScreen3();
|
std::unique_ptr<Screen> CreateScreen3();
|
||||||
|
|
91
src/DisplayApp/Screens/FirmwareValidation.cpp
Normal file
91
src/DisplayApp/Screens/FirmwareValidation.cpp
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
#include <libs/lvgl/lvgl.h>
|
||||||
|
#include "FirmwareValidation.h"
|
||||||
|
#include "../DisplayApp.h"
|
||||||
|
#include "../../Version.h"
|
||||||
|
#include "../../Components/FirmwareValidator/FirmwareValidator.h"
|
||||||
|
|
||||||
|
using namespace Pinetime::Applications::Screens;
|
||||||
|
extern lv_font_t jetbrains_mono_extrabold_compressed;
|
||||||
|
extern lv_font_t jetbrains_mono_bold_20;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
static void ButtonEventHandler(lv_obj_t * obj, lv_event_t event)
|
||||||
|
{
|
||||||
|
FirmwareValidation* screen = static_cast<FirmwareValidation *>(obj->user_data);
|
||||||
|
screen->OnButtonEvent(obj, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
FirmwareValidation::FirmwareValidation(Pinetime::Applications::DisplayApp *app,
|
||||||
|
Pinetime::Controllers::FirmwareValidator &validator)
|
||||||
|
: Screen{app}, validator{validator} {
|
||||||
|
labelVersionInfo = lv_label_create(lv_scr_act(), NULL);
|
||||||
|
lv_obj_align(labelVersionInfo, NULL, LV_ALIGN_IN_TOP_LEFT, 0, 0);
|
||||||
|
lv_label_set_text(labelVersionInfo, "Version : ");
|
||||||
|
lv_label_set_align(labelVersionInfo, LV_LABEL_ALIGN_LEFT);
|
||||||
|
|
||||||
|
|
||||||
|
labelVersionValue = lv_label_create(lv_scr_act(), NULL);
|
||||||
|
lv_obj_align(labelVersionValue, labelVersionInfo, LV_ALIGN_OUT_RIGHT_MID, 0, 0);
|
||||||
|
lv_label_set_recolor(labelVersionValue, true);
|
||||||
|
sprintf(version, "%d.%d.%d", Version::Major(), Version::Minor(), Version::Patch());
|
||||||
|
lv_label_set_text(labelVersionValue, version);
|
||||||
|
|
||||||
|
labelIsValidated = lv_label_create(lv_scr_act(), NULL);
|
||||||
|
lv_obj_align(labelIsValidated, NULL, LV_ALIGN_IN_TOP_LEFT, 0, 50);
|
||||||
|
lv_label_set_recolor(labelIsValidated, true);
|
||||||
|
lv_label_set_long_mode(labelIsValidated, LV_LABEL_LONG_BREAK);
|
||||||
|
lv_obj_set_width(labelIsValidated, 240);
|
||||||
|
|
||||||
|
if(validator.IsValidated())
|
||||||
|
lv_label_set_text(labelIsValidated, "You have already\n#00ff00 validated# this firmware#");
|
||||||
|
else {
|
||||||
|
lv_label_set_text(labelIsValidated,
|
||||||
|
"Please #00ff00 Validate# this version or\n#ff0000 Reset# to rollback to the previous version.");
|
||||||
|
|
||||||
|
buttonValidate = lv_btn_create(lv_scr_act(), NULL);
|
||||||
|
lv_obj_align(buttonValidate, NULL, LV_ALIGN_IN_BOTTOM_LEFT, 0, 0);
|
||||||
|
buttonValidate->user_data = this;
|
||||||
|
lv_obj_set_event_cb(buttonValidate, ButtonEventHandler);
|
||||||
|
|
||||||
|
labelButtonValidate = lv_label_create(buttonValidate, NULL);
|
||||||
|
lv_label_set_recolor(labelButtonValidate, true);
|
||||||
|
lv_label_set_text(labelButtonValidate, "#00ff00 Validate#");
|
||||||
|
|
||||||
|
buttonReset = lv_btn_create(lv_scr_act(), NULL);
|
||||||
|
buttonReset->user_data = this;
|
||||||
|
lv_obj_align(buttonReset, NULL, LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0);
|
||||||
|
lv_obj_set_event_cb(buttonReset, ButtonEventHandler);
|
||||||
|
|
||||||
|
labelButtonReset = lv_label_create(buttonReset, NULL);
|
||||||
|
lv_label_set_recolor(labelButtonReset, true);
|
||||||
|
lv_label_set_text(labelButtonReset, "#ff0000 Reset#");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FirmwareValidation::~FirmwareValidation() {
|
||||||
|
lv_obj_clean(lv_scr_act());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FirmwareValidation::Refresh() {
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FirmwareValidation::OnButtonPushed() {
|
||||||
|
running = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FirmwareValidation::OnButtonEvent(lv_obj_t *object, lv_event_t event) {
|
||||||
|
if(object == buttonValidate && event == LV_EVENT_PRESSED) {
|
||||||
|
validator.Validate();
|
||||||
|
running = false;
|
||||||
|
} else if(object == buttonReset && event == LV_EVENT_PRESSED) {
|
||||||
|
validator.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
42
src/DisplayApp/Screens/FirmwareValidation.h
Normal file
42
src/DisplayApp/Screens/FirmwareValidation.h
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include "Screen.h"
|
||||||
|
#include <bits/unique_ptr.h>
|
||||||
|
#include <libs/lvgl/src/lv_core/lv_style.h>
|
||||||
|
#include <libs/lvgl/src/lv_core/lv_obj.h>
|
||||||
|
|
||||||
|
namespace Pinetime {
|
||||||
|
namespace Controllers {
|
||||||
|
class FirmwareValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Applications {
|
||||||
|
namespace Screens {
|
||||||
|
|
||||||
|
class FirmwareValidation : public Screen{
|
||||||
|
public:
|
||||||
|
FirmwareValidation(DisplayApp* app, Pinetime::Controllers::FirmwareValidator& validator);
|
||||||
|
~FirmwareValidation() override;
|
||||||
|
|
||||||
|
bool Refresh() override;
|
||||||
|
bool OnButtonPushed() override;
|
||||||
|
|
||||||
|
void OnButtonEvent(lv_obj_t *object, lv_event_t event);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Pinetime::Controllers::FirmwareValidator& validator;
|
||||||
|
|
||||||
|
lv_obj_t* labelVersionInfo;
|
||||||
|
lv_obj_t* labelVersionValue;
|
||||||
|
char version[9];
|
||||||
|
lv_obj_t* labelIsValidated;
|
||||||
|
lv_obj_t* buttonValidate;
|
||||||
|
lv_obj_t* labelButtonValidate;
|
||||||
|
lv_obj_t* buttonReset;
|
||||||
|
lv_obj_t* labelButtonReset;
|
||||||
|
bool running = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ namespace Pinetime {
|
||||||
namespace Applications {
|
namespace Applications {
|
||||||
namespace Screens {
|
namespace Screens {
|
||||||
namespace Symbols {
|
namespace Symbols {
|
||||||
|
static constexpr char* none = "";
|
||||||
static constexpr char* batteryFull = "\xEF\x89\x80";
|
static constexpr char* batteryFull = "\xEF\x89\x80";
|
||||||
static constexpr char* batteryEmpty = "\xEF\x89\x84";
|
static constexpr char* batteryEmpty = "\xEF\x89\x84";
|
||||||
static constexpr char* batteryThreeQuarter = "\xEF\x89\x81";
|
static constexpr char* batteryThreeQuarter = "\xEF\x89\x81";
|
||||||
|
|
|
@ -58,14 +58,6 @@ void SystemTask::Work() {
|
||||||
|
|
||||||
spi.Init();
|
spi.Init();
|
||||||
spiNorFlash.Init();
|
spiNorFlash.Init();
|
||||||
|
|
||||||
// Write the 'image OK' flag if it's not already done
|
|
||||||
// TODO implement a better verification mecanism for the image (ask for user confirmation via UI/BLE ?)
|
|
||||||
uint32_t* imageOkPtr = reinterpret_cast<uint32_t *>(0x7BFE8);
|
|
||||||
uint32_t imageOk = *imageOkPtr;
|
|
||||||
if(imageOk != 1)
|
|
||||||
Pinetime::Drivers::InternalFlash::WriteWord(0x7BFE8, 1);
|
|
||||||
|
|
||||||
nimbleController.Init();
|
nimbleController.Init();
|
||||||
nimbleController.StartAdvertising();
|
nimbleController.StartAdvertising();
|
||||||
lcd.Init();
|
lcd.Init();
|
||||||
|
|
Loading…
Reference in New Issue
Block a user