diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d60323b..67462741 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,7 +129,7 @@ target_sources (HostUDD PRIVATE ) -add_library (objSMCE OBJECT) +add_library (objSMCE OBJECT ) configure_coverage (objSMCE) set_property (TARGET objSMCE PROPERTY CXX_EXTENSIONS Off) set_property (TARGET objSMCE PROPERTY POSITION_INDEPENDENT_CODE True) @@ -153,6 +153,7 @@ target_sources (objSMCE PRIVATE include/SMCE/SketchConf.hpp include/SMCE/internal/BoardDeviceSpecification.hpp ) + if (NOT MSVC) target_compile_options (objSMCE PRIVATE "-Wall" "-Wextra" "-Wpedantic" "-Werror" "-Wcast-align") else () diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 00000000..e4686fc5 --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1,49 @@ +# +# client/CMakeLists.txt +# Written by Rylander and Mejborn, Team 1, DAT265. +# +# + + +cmake_minimum_required (VERSION 3.16) +project (SMCE_client) + +find_package (Threads REQUIRED) +find_package (SMCE REQUIRED) + +set (SMCE_RES "${PROJECT_BINARY_DIR}/SMCE_Res") + +include (FetchContent) +FetchContent_Declare (Termcolor + GIT_REPOSITORY "https://github.com/ikalnytskyi/termcolor" + GIT_TAG "v2.0.0" + GIT_SHALLOW On + GIT_PROGRESS On + ) +FetchContent_Declare (Lyra + GIT_REPOSITORY "https://github.com/bfgroup/Lyra" + GIT_TAG "1.5.1" + GIT_SHALLOW On + GIT_PROGRESS On + ) +FetchContent_MakeAvailable(Termcolor Lyra) + +add_executable (${PROJECT_NAME} SMCE_Client.cpp) +target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src) +target_sources (${PROJECT_NAME} PRIVATE + src/UartToFile.hpp + src/UartToFile.cpp + ) +target_link_libraries (${PROJECT_NAME} PRIVATE SMCE::SMCE termcolor::termcolor bfg::Lyra) +target_include_directories (${PROJECT_NAME} PUBLIC + "${termcolor_SOURCE_DIR}/include/termcolor" + "${lyra_SOURCE_DIR}/include/lyra" + ) +target_compile_definitions (${PROJECT_NAME} PRIVATE "SMCE_RESOURCES_DIR=\"${SMCE_RES}\"") +file (MAKE_DIRECTORY "${SMCE_RES}") +execute_process (COMMAND "${CMAKE_COMMAND}" -E tar xf "${SMCE_RESOURCES_ARK}" + WORKING_DIRECTORY "${SMCE_RES}") + +add_custom_command (TARGET ${PROJECT_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$" "$" + ) \ No newline at end of file diff --git a/client/README.md b/client/README.md new file mode 100644 index 00000000..806d6ad7 --- /dev/null +++ b/client/README.md @@ -0,0 +1,62 @@ +# libSMCE command line frontend + +Like SMCE, this command line frontend requires a C++20 toolchain. + +**IMPORTANT** +As is, SMCE_Client is not guaranteed to work on Windows machines! +Due to a bug that causes the running sketch to crash when sending or reciving messages on uart. + + +## Build instructions + +```shell +cd client +cmake -S . -B build/ +cmake --build build/ +``` + +You can now find the executable "SMCE_client" in the `./build` directory. + +## Run instructions +``` +SMCE_client -f -p +``` +where +- FQBN: [Fully Qualified Board Name](https://arduino.github.io/arduino-cli/latest/FAQ/#whats-the-fqbn-string) + Testing has been done with fqbn = arduino:sam:arduino_due_x +- Sketch path: Relative or absolute path to the sketch to run + +### Start arguments +-f,--fqbn -> = Fully qualified board name +-p,--path -> = Relative or absolute path to the sketch to run +-d,--dir -> = Relative or absolute path to desired location of arduino root folder +-s,--SMCE -> = Relative or absolute path to SMCE\_RESOURCE folder +-u,--file -> = Set to true if uart should write to file (created in the set arduino root folder) + +(-s or -- SMCE, can be used if binary is not compiled and already linked to the SMCE_RESOURCE folder for the current computer.) + +## Configuration of board + +As is, configuring of GPIO pins on the board is done in the source file SMCE_Client.cpp, as seen here: + +``` +smce::BoardConfig board_conf{ + .pins = {0,1}, + .gpio_drivers = { + smce::BoardConfig::GpioDrivers{0, + smce::BoardConfig::GpioDrivers::DigitalDriver{true,true}, + smce::BoardConfig::GpioDrivers::AnalogDriver{false,false} + }, + ... + ... + } +``` +.pins = a list of all a pins on the board. +.gpio_drivers = specifies the drivers for each pin, configured as: +``` +smce::BoardConfig::GpioDrivers{, + smce::BoardConfig::GpioDrivers::DigitalDriver{,}, + smce::BoardConfig::GpioDrivers::AnalogDriver{,} + } +``` +DigitalDriver and AnalogDriver has two parameters {,}, these are set as true or false depending on what the pin should be able to do. \ No newline at end of file diff --git a/client/SMCE_Client.cpp b/client/SMCE_Client.cpp new file mode 100644 index 00000000..73d1c818 --- /dev/null +++ b/client/SMCE_Client.cpp @@ -0,0 +1,294 @@ +/* + * client/SMCE_Client.cpp + * Created by Rylander and Mejborn, Team 1, DAT265. + * + * A terminal interface for libSMCE, that allows sketches to be ran without the use of smce-gd. + * Functionally to test and debug sketches, as GPIO pins can be set with values and + * messages can be sent to board through uart. + * + */ + +#ifndef SMCE_RESOURCES_DIR +# error "SMCE_RESOURCES_DIR is not set" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::literals; + +void exit_sketch(int code) { std::cerr << "Error code: " << code << std::endl; } + +// atomic bool for handling threads +static std::atomic_bool run_threads = true; +static std::atomic_bool mute_uart = false; +static std::mutex t_lock; +// Listen to the uart input from board, writes what it read to terminal +void uart_listener(smce::VirtualUart uart, bool file_write, std::string path) { + auto tx = uart.tx(); + std::string file = path + "/uartlog" + get_time() + ".txt"; + while (run_threads) { + std::string buffer; + t_lock.lock(); // if recompiling sketch, lock thread here until recompiling is done. + buffer.resize(tx.max_size()); + const auto len = tx.read(buffer); + t_lock.unlock(); + if (len == 0) { + std::this_thread::sleep_for(1ms); + continue; + } + buffer.resize(len); + if (file_write) { + uart_to_file(buffer, file); + } else if (!mute_uart) { + std::cout << termcolor::red << buffer << termcolor::reset << std::endl << "$>" << std::flush; + } + } +} +// Prints a command menu for SMCE_Client +void print_menu() { + std::cout << "SMCE Client menu:\n" + << "-h -> See menu\n" + << "-p -> Pause or resume the board\n" + << "-rc -> Recompile the sketch\n" + << "-m -> Send message to board through uart (serial)\n" + << "-mt -> disable uart prints in console\n" + << "-wa -> Set a specific value on a analog pin\n" + << "-wd -> Set a specific value on a digital pin, value should be 0 or 1\n" + << "-ra -> Read the value on a analog pin\n" + << "-rd -> Read the value on a digital pin\n" + << "-q -> Power off board and quit program \n"; +} + +int compile_sketch(smce::Sketch& sketch, smce::Toolchain& toolchain, smce::Board& board, std::string arduino_root_dir) { + // Compile the sketch on the toolchain + if (const auto ec = toolchain.compile(sketch)) { + std::cerr << "Error: " << ec.message() << std::endl; + auto [_, log] = toolchain.build_log(); + if (!log.empty()) + std::cerr << log << std::endl; + return EXIT_FAILURE; + } + board.attach_sketch(sketch); + smce::BoardConfig board_conf{ + .pins = {0, 1}, // Creating pins & GPIO drivers + .gpio_drivers = {smce::BoardConfig::GpioDrivers{10, smce::BoardConfig::GpioDrivers::DigitalDriver{true, true}, + smce::BoardConfig::GpioDrivers::AnalogDriver{true, true}}, + smce::BoardConfig::GpioDrivers{1, smce::BoardConfig::GpioDrivers::DigitalDriver{false, true}, + smce::BoardConfig::GpioDrivers::AnalogDriver{false, true}}}, + .uart_channels = {{}}, // use standard configuration of uart_channels + .sd_cards = {smce::BoardConfig::SecureDigitalStorage{.root_dir = arduino_root_dir}}}; + board.configure(std::move(board_conf)); + return EXIT_SUCCESS; +} + +int compile_and_start(smce::Sketch& sketch, smce::Toolchain& toolchain, smce::Board& board, + std::string arduino_root_dir) { + if (board.status() == smce::Board::Status::running) { + board.stop(); + board.reset(); + } + compile_sketch(sketch, toolchain, board, arduino_root_dir); + // Start board + if (!board.start()) { + std::cerr << "Error: Board failed to start sketch" << std::endl; + return EXIT_FAILURE; + } else { + return EXIT_SUCCESS; + } +} + +int main(int argc, char** argv) { + + std::string fqbn; + std::string path_to_sketch; + std::string arduino_root_dir = "."; // path to root dir for arduino, standard is in start folder. + std::string smce_resource_dir = SMCE_RESOURCES_DIR; // smce_resource_dir path, given at runtime. + bool file_write = false; // DEFAULT is to write in console + bool show_help = false; + + // Setup lyra start arguments for the parser. + auto cli = + lyra::help(show_help) | lyra::opt(fqbn, "fqbn")["--fpqn"]["-f"]("Fully qualified board name") | + lyra::opt(path_to_sketch, "path-to-sketch")["--path"]["-p"]("The path to the sketch") | + lyra::opt(arduino_root_dir, + "Ardunio home folder")["--dir"]["-d"]("The absolute path to the desired location of Arduino root") | + lyra::opt(smce_resource_dir, + "Alternative path to SMCE_RESOURCE") // Makes it possible to change path to SMCE_RESOURCES at start. + ["--SMCE"]["-s"]("Tha alternative path to SMCE_RESOURCES for runtime, should be absolute.") | + lyra::opt(file_write, "file_write")["--file"]["-u"]( + "Set to true if uart should write to file (created in the set Arduino root folder)"); + + auto result = cli.parse({argc, argv}); + + // If something is not right with parser of input, show error + if (!result) { + std::cerr << "Error in command line: " << result.errorMessage() << std::endl; + return 1; + } + // If user input -h or --help, display help. + if (show_help) { + std::cout << cli << "\n"; + return 0; + } + + std::cout << std::endl << "Starting SMCE Client" << std::endl; + std::cout << "Given Fqbn: " << fqbn << std::endl << "Given path to sketch: " << path_to_sketch << std::endl; + + // Create the toolchain + smce::Toolchain toolchain{smce_resource_dir}; + if (const auto ec = toolchain.check_suitable_environment()) { + std::cerr << "Error: " << ec.message() << std::endl; + return EXIT_FAILURE; + } + + // Create the sketch, and declare that it requires the WiFi and MQTT Arduino libraries during preprocessing + // clang-format off + smce::Sketch sketch{path_to_sketch, {.fqbn = fqbn, .legacy_preproc_libs = {{"WiFi"}, {"MQTT"}}}}; + // // clang-format on + + // Create the virtual Arduino board + std::cout << "Creating board and compiling sketch" << std::endl; + smce::Board board{exit_sketch}; + + // Compile and start board + compile_and_start(sketch, toolchain, board, arduino_root_dir); + std::cout << "Complete" << std::endl; + // Create view and uart (serial) channels + auto board_view = board.view(); + auto uart0 = board_view.uart_channels[0]; + + // start listener thread for uart + std::thread uart_thread{[=] { uart_listener(uart0, file_write, arduino_root_dir); }}; + std::cout << "Messages received on uart from arduino is shown as " << termcolor::red << "red" << termcolor::reset + << " text." << std::endl; + // Print command menu + print_menu(); + // Main loop, handle the command input + while (true) { + std::cout << "$>"; + std::string input; + std::getline(std::cin, input); + board.tick(); + if (input == "-h") { // help menu + print_menu(); + + } else if (input == "-p") { // pause or resume + if (board.status() != smce::Board::Status::suspended) { + if (board.suspend()) + std::cout << "Board paused" << std::endl; + else + std::cout << "Board could not be paused" << std::endl; + } else if (board.status() == smce::Board::Status::suspended) { + board.resume(); + std::cout << "Board resumed" << std::endl; + } + } else if (input == "-rc") { // recompile + std::cout << "Recompiling.." << std::endl; + t_lock.lock(); // aquire lock before recompiling, so uart_listener is forced to wait + compile_and_start(sketch, toolchain, board, arduino_root_dir); + // update boardview and uart0 after the sketch has been recompiled. + board_view = board.view(); + uart0 = board_view.uart_channels[0]; + t_lock.unlock(); // unlock as recompile is done + std::cout << "Complete" << std::endl; + + } else if (input.starts_with("-m ")) { // send message on uart + std::string message = input.substr(3); + for (std::span to_write = message; !to_write.empty();) { + const auto written_count = uart0.rx().write(to_write); + to_write = to_write.subspan(written_count); + } + + } else if (input.starts_with("-mt")) { + if (mute_uart) { + mute_uart = false; + std::cout << "Unmuted uart" << std::endl; + } else { + mute_uart = true; + std::cout << "Muted uart" << std::endl; + } + + } else if (input.starts_with("-wa ")) { // write value on analog pin + const int pin = stoi(input.substr(3, 5)); + const uint16_t value = stoi(input.substr(5)); + auto apin = board_view.pins[pin].analog(); + if (!apin.exists()) { + std::cout << "Pin does not exist!" << std::endl; + }else if (!apin.can_write()) { + std::cout << "Can't write to pin!" << std::endl; + }else{ + apin.write(value); + } + + } else if (input.starts_with("-wd ")) { // write value on digital pin + const int pin = stoi(input.substr(3, 5)); + const uint16_t value = stoi(input.substr(5)); + auto apin = board_view.pins[pin].digital(); + if (!apin.exists()) { + std::cout << "Pin does not exist!" << std::endl; + }else if (!apin.can_write()) { + std::cout << "Can't write to pin!" << std::endl; + }else{ + if (value == 0) { + apin.write(false); + } else if (value == 1) { + apin.write(true); + } else { + std::cout << "Value must be 0 or 1 for digital pins" << std::endl; + } + } + + } else if (input.starts_with("-ra ")) { + const int index_pin = stoi(input.substr(3)); + auto pin = board_view.pins[index_pin].analog(); + if (!pin.exists()) { + std::cout << "Pin does not exist!" << std::endl; + + }else if (!pin.can_read()) { + std::cout << "Can't read from pin!" << std::endl; + }else{ + std::cout << "Value from pin " << index_pin << " is " << pin.read() << std::endl; + } + + } else if (input.starts_with("-rd ")) { + const int index_pin = stoi(input.substr(3)); + auto pin = board_view.pins[index_pin].digital(); + if (!pin.exists()) { + std::cout << "Pin does not exist!" << std::endl; + }else if (!pin.can_read()) { + std::cout << "Can't read from pin!" << std::endl; + }else { + std::cout << "Value from pin " << index_pin << " is " << pin.read() << std::endl; + } + + } else if (input == "-q") { // power off and quit + std::cout << "Quitting..." << std::endl; + run_threads = false; + board.stop(); + uart_thread.join(); + break; + + } else { + // If input don't match with anything. + std::cout << "Unknown input, try again." << std::endl; + } + } +} \ No newline at end of file diff --git a/client/src/UartToFile.cpp b/client/src/UartToFile.cpp new file mode 100644 index 00000000..2dc8195f --- /dev/null +++ b/client/src/UartToFile.cpp @@ -0,0 +1,22 @@ +#include +#include +#include +#include +#include + + +std::string get_time(){ + auto time_t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + std::tm time = *std::localtime(&time_t); + std::stringstream ss; + ss << std::put_time(&time, "%T"); + return ss.str(); +} + +int uart_to_file(std::string message, std::string path){ + std::ofstream file; + file.open(path, std::ios_base::app); + file << get_time() +": " +message + "\n"; + file.close(); + return 0; +} diff --git a/client/src/UartToFile.hpp b/client/src/UartToFile.hpp new file mode 100644 index 00000000..eb36a204 --- /dev/null +++ b/client/src/UartToFile.hpp @@ -0,0 +1,12 @@ + + +#ifndef SMCE_UARTTOFILE_HPP +#define SMCE_UARTTOFILE_HPP + +#include + +std::string get_time(); + +int uart_to_file(std::string message,std::string path); + +#endif \ No newline at end of file