mirror of
https://github.com/HendrikRauh/dmx-interface.git
synced 2026-04-07 10:22:21 +00:00
Merge branch 'idf' into idf-dmx
This commit is contained in:
commit
573757ffe0
18 changed files with 443 additions and 103 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,6 +4,7 @@
|
|||
/build/
|
||||
*.elf
|
||||
*.bin
|
||||
*.uf2
|
||||
*.hex
|
||||
*.map
|
||||
*.img
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ exclude: |
|
|||
.*\.o|
|
||||
flake.lock|
|
||||
dependencies.lock|
|
||||
assets/case/
|
||||
assets/case/|
|
||||
docs/doxygen/
|
||||
)
|
||||
|
||||
repos:
|
||||
|
|
@ -33,7 +34,6 @@ repos:
|
|||
args: [--maxkb=1000]
|
||||
- id: check-ast
|
||||
- id: check-case-conflict
|
||||
- id: check-docstring-first
|
||||
- id: check-json
|
||||
- id: check-merge-conflict
|
||||
- id: detect-private-key
|
||||
|
|
@ -99,3 +99,22 @@ repos:
|
|||
entry: nixfmt
|
||||
language: system
|
||||
types: [nix]
|
||||
|
||||
# CMake formatting and linting
|
||||
- repo: https://github.com/cheshirekow/cmake-format-precommit
|
||||
rev: v0.6.10
|
||||
hooks:
|
||||
- id: cmake-format
|
||||
- id: cmake-lint
|
||||
|
||||
# Doxygen code coverage
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: doxygen-coverage
|
||||
name: doxygen code coverage
|
||||
language: system
|
||||
entry: tools/doxy-coverage.py
|
||||
args: [docs/doxygen/xml, --threshold=40, --generate-docs]
|
||||
types_or: [c, c++, header]
|
||||
verbose: false
|
||||
require_serial: true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Ignore build artifacts and generated files
|
||||
build/
|
||||
managed_components/
|
||||
docs/doxygen/
|
||||
docs/external/
|
||||
.direnv/
|
||||
*.lock
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
# For more information about build system see
|
||||
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
|
||||
# The following five lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# Clear any stale EXTRA_COMPONENT_DIRS entries (e.g. leftover from previous runs or PlatformIO)
|
||||
# Clear any stale EXTRA_COMPONENT_DIRS entries (e.g. leftover from previous runs
|
||||
# or PlatformIO)
|
||||
set(EXTRA_COMPONENT_DIRS "")
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
|
@ -13,5 +10,6 @@ project(dmx-interface)
|
|||
|
||||
# Enable LittleFS filesystem image creation for web assets
|
||||
if(COMMAND littlefs_create_partition_image)
|
||||
littlefs_create_partition_image(storage ${CMAKE_SOURCE_DIR}/data FLASH_IN_PROJECT)
|
||||
littlefs_create_partition_image(storage ${CMAKE_SOURCE_DIR}/data
|
||||
FLASH_IN_PROJECT)
|
||||
endif()
|
||||
|
|
|
|||
1
Doxyfile
1
Doxyfile
|
|
@ -17,6 +17,7 @@ USE_MDFILE_AS_MAINPAGE = README.md
|
|||
# Documentation settings
|
||||
GENERATE_LATEX = NO
|
||||
GENERATE_HTML = YES
|
||||
GENERATE_XML=YES
|
||||
|
||||
# doxygen-awesome-css settings
|
||||
HTML_EXTRA_STYLESHEET = docs/external/doxygen-awesome-css/doxygen-awesome.css \
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
| 1x | ♂️-DMX-socket |
|
||||
| 1x | ♀️-DMX-socket |
|
||||
|
||||
> Additionally you need: `some wires`, `soldering equipment`, `3D-printer`, `small screws`, `shrink tubing`, `hot glue gun`
|
||||
> Additionally you need: `some wires`, `soldering equipment`, `3D-printer`, `small screws` (see [case](#-case)), `heat shrink tubing`, `hot glue gun`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -123,6 +123,13 @@ All print files (STL, STEP, X_T) can be found in [assets/case](/assets/case/). A
|
|||
|
||||

|
||||
|
||||
| Part | Screw | Count |
|
||||
| ----------- | ------- | ----- |
|
||||
| Case lid | M2x5 | 4x |
|
||||
| ESP32 | M2x5 | 2x |
|
||||
| W5500 | M2,5x5 | 2x |
|
||||
| XLR sockets | M3+Nuts | 4x |
|
||||
|
||||
---
|
||||
|
||||
## 💡 Status LED
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
/**
|
||||
* @file logger.h
|
||||
* @brief Project-wide logging macros based on ESP-IDF's logging library.
|
||||
|
|
@ -27,8 +26,12 @@
|
|||
|
||||
#include "esp_log.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef LOG_TAG
|
||||
#define LOG_TAG "CHAOS"
|
||||
#define LOG_TAG "CHAOS" ///< Default log tag
|
||||
#endif
|
||||
|
||||
/** @brief Log a message at Error level. */
|
||||
|
|
@ -41,3 +44,7 @@
|
|||
#define LOGD(...) ESP_LOGD(LOG_TAG, __VA_ARGS__)
|
||||
/** @brief Log a message at Verbose level. */
|
||||
#define LOGV(...) ESP_LOGV(LOG_TAG, __VA_ARGS__)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
idf_component_register(
|
||||
SRCS "src/storage.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES joltwallet__littlefs
|
||||
PRIV_REQUIRES vfs logger
|
||||
)
|
||||
SRCS
|
||||
"src/storage.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
REQUIRES
|
||||
joltwallet__littlefs
|
||||
PRIV_REQUIRES
|
||||
vfs
|
||||
logger)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
#ifndef STORAGE_H
|
||||
#define STORAGE_H
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
|
|
@ -24,5 +23,3 @@ const char *storage_get_mount_point(void);
|
|||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* STORAGE_H */
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
#define LOG_TAG "STORE"
|
||||
#define LOG_TAG "STORE" ///< "STORE" log tag for this file
|
||||
|
||||
#include "storage.h"
|
||||
#include "esp_littlefs.h"
|
||||
#include "esp_vfs.h"
|
||||
#include "logger.h"
|
||||
|
||||
static const char *LITTLEFS_MOUNT_POINT = "/data";
|
||||
static const char *LITTLEFS_MOUNT_POINT =
|
||||
"/data"; ///< Mount point for LittleFS filesystem
|
||||
|
||||
esp_err_t storage_init(void) {
|
||||
esp_vfs_littlefs_conf_t conf = {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
idf_component_register(
|
||||
SRCS "src/web_server.c"
|
||||
SRCS
|
||||
"src/web_server.c"
|
||||
"src/wifi.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_http_server storage
|
||||
PRIV_REQUIRES freertos esp_wifi esp_event esp_netif nvs_flash logger
|
||||
)
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
REQUIRES
|
||||
esp_http_server
|
||||
storage
|
||||
PRIV_REQUIRES
|
||||
freertos
|
||||
esp_wifi
|
||||
esp_event
|
||||
esp_netif
|
||||
nvs_flash
|
||||
logger)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,26 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Start WiFi Access Point (AP) mode.
|
||||
*
|
||||
* Initializes and starts the WiFi AP with the given SSID and password.
|
||||
*
|
||||
* @param ssid SSID for the AP (1-32 characters)
|
||||
* @param password Password for the AP (min. 8 characters, optional)
|
||||
* @param channel WiFi channel to use
|
||||
* @param max_connections Maximum number of client connections
|
||||
* @return ESP_OK on success, ESP_ERR_INVALID_ARG or other error codes on
|
||||
* failure
|
||||
*/
|
||||
esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel,
|
||||
uint8_t max_connections);
|
||||
|
||||
/**
|
||||
* @brief Stop WiFi Access Point (AP) mode.
|
||||
*
|
||||
* Deinitializes and stops the WiFi AP.
|
||||
*/
|
||||
void wifi_stop_ap(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
#define LOG_TAG "WEBSRV"
|
||||
|
||||
/**
|
||||
* @def LOG_TAG
|
||||
* @brief Tag used for web server logging.
|
||||
*/
|
||||
|
||||
#include "web_server.h"
|
||||
|
||||
#include <ctype.h>
|
||||
|
|
@ -14,12 +19,34 @@
|
|||
#include "storage.h"
|
||||
|
||||
// Default configuration values
|
||||
/**
|
||||
* @brief Default port for the web server.
|
||||
*/
|
||||
#define WEBSERVER_DEFAULT_PORT 80
|
||||
/**
|
||||
* @brief Default port for the web server.
|
||||
*/
|
||||
#define WEBSERVER_DEFAULT_MAX_HANDLERS 32
|
||||
/**
|
||||
* @brief Default maximum number of URI handlers.
|
||||
*/
|
||||
#define WEBSERVER_DEFAULT_STACK_SIZE (8 * 1024)
|
||||
/**
|
||||
* @brief Default stack size for the web server task.
|
||||
*/
|
||||
#define WEBSERVER_DEFAULT_TASK_PRIORITY 5
|
||||
/**
|
||||
* @brief Default task priority for the web server task.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Handle for the HTTP server instance.
|
||||
*/
|
||||
static httpd_handle_t s_server_handle = NULL;
|
||||
|
||||
/**
|
||||
* @brief Handle for the FreeRTOS web server task.
|
||||
*/
|
||||
static TaskHandle_t s_server_task_handle = NULL;
|
||||
|
||||
/**
|
||||
|
|
@ -127,6 +154,15 @@ static void webserver_task(void *arg) {
|
|||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start the web server with the given configuration.
|
||||
*
|
||||
* Initializes storage, configures the HTTP server, registers default handlers,
|
||||
* and starts the FreeRTOS task for async operation.
|
||||
*
|
||||
* @param config Pointer to webserver configuration struct (optional)
|
||||
* @return Handle to the running HTTP server, or NULL on failure
|
||||
*/
|
||||
httpd_handle_t webserver_start(const webserver_config_t *config) {
|
||||
if (s_server_handle != NULL) {
|
||||
LOGW("Web server already running");
|
||||
|
|
@ -207,6 +243,13 @@ httpd_handle_t webserver_start(const webserver_config_t *config) {
|
|||
return s_server_handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop the web server and clean up resources.
|
||||
*
|
||||
* Stops the HTTP server and deletes the FreeRTOS task.
|
||||
*
|
||||
* @param server Handle to the HTTP server instance
|
||||
*/
|
||||
void webserver_stop(httpd_handle_t server) {
|
||||
if (server == NULL) {
|
||||
return;
|
||||
|
|
@ -224,6 +267,14 @@ void webserver_stop(httpd_handle_t server) {
|
|||
LOGI("Web server stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Register a URI handler with the web server.
|
||||
*
|
||||
* @param server Handle to the HTTP server instance
|
||||
* @param uri_handler Pointer to the URI handler struct
|
||||
* @return ESP_OK on success, ESP_ERR_INVALID_ARG or other error codes on
|
||||
* failure
|
||||
*/
|
||||
esp_err_t webserver_register_handler(httpd_handle_t server,
|
||||
const httpd_uri_t *uri_handler) {
|
||||
if (server == NULL || uri_handler == NULL) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
#define LOG_TAG "WIFI"
|
||||
|
||||
#include "wifi.h"
|
||||
#define LOG_TAG "WIFI" ///< "WIFI" log tag for this file
|
||||
|
||||
#include <string.h>
|
||||
|
||||
|
|
@ -9,9 +7,25 @@
|
|||
#include "esp_wifi.h"
|
||||
#include "logger.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "wifi.h"
|
||||
|
||||
/**
|
||||
* @brief Indicates whether the WiFi AP is started.
|
||||
*/
|
||||
static bool s_wifi_started = false;
|
||||
|
||||
/**
|
||||
* @brief Start WiFi Access Point (AP) mode.
|
||||
*
|
||||
* Initializes and starts the WiFi AP with the given SSID and password.
|
||||
*
|
||||
* @param ssid SSID for the AP (1-32 characters)
|
||||
* @param password Password for the AP (min. 8 characters, optional)
|
||||
* @param channel WiFi channel to use
|
||||
* @param max_connections Maximum number of client connections
|
||||
* @return ESP_OK on success, ESP_ERR_INVALID_ARG or other error codes on
|
||||
* failure
|
||||
*/
|
||||
esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel,
|
||||
uint8_t max_connections) {
|
||||
if (s_wifi_started) {
|
||||
|
|
@ -74,6 +88,11 @@ esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel,
|
|||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop WiFi Access Point (AP) mode.
|
||||
*
|
||||
* Deinitializes and stops the WiFi AP.
|
||||
*/
|
||||
void wifi_stop_ap(void) {
|
||||
if (!s_wifi_started) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
idf_component_register(SRCS "dmx-interface.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES web_server dmx)
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"dmx-interface.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
REQUIRES
|
||||
dmx
|
||||
logger
|
||||
web_server)
|
||||
|
|
|
|||
|
|
@ -1,33 +1,39 @@
|
|||
#define LOG_TAG "MAIN" ///< "MAIN" log tag for this file
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include "dmx.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "logger.h"
|
||||
#include "web_server.h"
|
||||
#include "wifi.h"
|
||||
|
||||
static const char *TAG = "MAIN";
|
||||
|
||||
/**
|
||||
* @brief Main entry point for the DMX Interface application.
|
||||
*
|
||||
* Initializes WiFi Access Point and starts the web server.
|
||||
* Keeps the application running indefinitely.
|
||||
*/
|
||||
void app_main(void) {
|
||||
ESP_LOGI(TAG, "DMX Interface starting...");
|
||||
LOGI("DMX Interface starting...");
|
||||
|
||||
esp_err_t wifi_err = wifi_start_ap("DMX", "mbgmbgmbg", 1, 4);
|
||||
if (wifi_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start WiFi AP: %s", esp_err_to_name(wifi_err));
|
||||
LOGE("Failed to start WiFi AP: %s", esp_err_to_name(wifi_err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Start HTTP web server
|
||||
httpd_handle_t server = webserver_start(NULL);
|
||||
if (server == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to start web server!");
|
||||
LOGE("Failed to start web server!");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Web server started successfully");
|
||||
ESP_LOGI(TAG, "Open http://192.168.4.1 in your browser");
|
||||
LOGI("Web server started successfully");
|
||||
LOGI("Open http://192.168.4.1 in your browser");
|
||||
|
||||
init_dmx(DMX_NUM_1, 21, 33);
|
||||
|
||||
|
|
|
|||
72
tasks.py
72
tasks.py
|
|
@ -1,8 +1,8 @@
|
|||
from invoke import task
|
||||
from invoke.exceptions import Exit
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import webbrowser
|
||||
|
||||
|
||||
|
|
@ -85,12 +85,8 @@ def reset(c):
|
|||
|
||||
@task
|
||||
def format(c):
|
||||
"""Format all source files using pre-commit hooks and optimize SVGs"""
|
||||
print("\nOptimizing SVG files...")
|
||||
c.run(
|
||||
"find . -name '*.svg' -not -path './build/*' -not -path './managed_components/*' | xargs -r svgo --quiet --final-newline",
|
||||
warn=True,
|
||||
)
|
||||
"""Format all source files using pre-commit hooks"""
|
||||
|
||||
is_windows = os.name == "nt"
|
||||
if is_windows:
|
||||
# Windows doesn't provide a POSIX pty
|
||||
|
|
@ -99,60 +95,6 @@ def format(c):
|
|||
c.run("pre-commit run --all-files", pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def format_check(c):
|
||||
"""Check if all files are formatted correctly"""
|
||||
missing_tools = []
|
||||
format_errors = []
|
||||
|
||||
print("Checking C file formatting...")
|
||||
result = c.run(
|
||||
"find main components -name '*.c' -o -name '*.h' | xargs clang-format --dry-run --Werror",
|
||||
warn=True,
|
||||
)
|
||||
if result:
|
||||
if result.return_code == 127: # Command not found
|
||||
missing_tools.append("clang-format")
|
||||
elif not result.ok:
|
||||
format_errors.append("C files")
|
||||
|
||||
print("Checking Python file formatting...")
|
||||
result = c.run("black --check tasks.py", warn=True)
|
||||
if result:
|
||||
if result.return_code == 127:
|
||||
missing_tools.append("black")
|
||||
elif not result.ok:
|
||||
format_errors.append("Python files")
|
||||
|
||||
print("Checking Nix file formatting...")
|
||||
result = c.run("nixfmt --check flake.nix", warn=True)
|
||||
if result:
|
||||
if result.return_code == 127:
|
||||
missing_tools.append("nixfmt")
|
||||
elif not result.ok:
|
||||
format_errors.append("Nix files")
|
||||
|
||||
print("Checking other file formatting...")
|
||||
result = c.run("prettier --check '**/*.{js,json,yaml,yml,md,html,css}'", warn=True)
|
||||
if result:
|
||||
if result.return_code == 127:
|
||||
missing_tools.append("prettier")
|
||||
elif not result.ok:
|
||||
format_errors.append("JS/JSON/YAML/HTML/CSS files")
|
||||
|
||||
if missing_tools:
|
||||
print(f"\n❌ ERROR: Missing formatting tools: {', '.join(missing_tools)}")
|
||||
print("Please install them or reload the nix-shell.")
|
||||
sys.exit(1)
|
||||
|
||||
if format_errors:
|
||||
print(f"\n❌ ERROR: Formatting issues in: {', '.join(format_errors)}")
|
||||
print("Run 'invoke format' to fix them.")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n✅ All files are correctly formatted!")
|
||||
|
||||
|
||||
@task(help={"o": "Open documentation in the default browser after generation."})
|
||||
def docs(c, o=False):
|
||||
"""Generate Doxygen documentation."""
|
||||
|
|
@ -164,3 +106,11 @@ def docs(c, o=False):
|
|||
webbrowser.open(f"file://{os.path.abspath(path)}")
|
||||
return
|
||||
raise Exit(code=1)
|
||||
|
||||
|
||||
@task
|
||||
def docs_coverage(c):
|
||||
"""List doxygen coverage of documentation."""
|
||||
subprocess.run(
|
||||
"python tools/doxy-coverage.py docs/doxygen/xml --no-error", shell=True
|
||||
)
|
||||
|
|
|
|||
244
tools/doxy-coverage.py
Executable file
244
tools/doxy-coverage.py
Executable file
|
|
@ -0,0 +1,244 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# -*- mode: python; coding: utf-8 -*-
|
||||
|
||||
# Based on https://github.com/davatorium/doxy-coverage
|
||||
# which was forked from https://github.com/alobbs/doxy-coverage
|
||||
# and modified for use in this project.
|
||||
|
||||
# All files in doxy-coverage are Copyright 2014 Alvaro Lopez Ortega.
|
||||
#
|
||||
# Authors:
|
||||
# * Alvaro Lopez Ortega <alvaro@gnu.org>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
__author__ = "Alvaro Lopez Ortega"
|
||||
__email__ = "alvaro@alobbs.com"
|
||||
__copyright__ = "Copyright (C) 2014 Alvaro Lopez Ortega"
|
||||
|
||||
from filecmp import cmp
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# Defaults
|
||||
ACCEPTABLE_COVERAGE = 80
|
||||
|
||||
# Global
|
||||
ns = None
|
||||
|
||||
|
||||
def ERROR(*objs):
|
||||
print("ERROR: ", *objs, end="\n", file=sys.stderr)
|
||||
|
||||
|
||||
def FATAL(*objs):
|
||||
ERROR(*objs)
|
||||
sys.exit(0 if ns.no_error else 1)
|
||||
|
||||
|
||||
def generate_docs():
|
||||
print("Generating Doxygen documentation...")
|
||||
proc = subprocess.run(
|
||||
"doxygen Doxyfile",
|
||||
shell=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
print("Documentation generated")
|
||||
else:
|
||||
FATAL("Failed to generate documentation. Exiting.")
|
||||
|
||||
|
||||
def parse_file(fullpath):
|
||||
tree = ET.parse(fullpath)
|
||||
|
||||
sourcefile = None
|
||||
definitions = {}
|
||||
|
||||
for definition in tree.findall("./compounddef//memberdef"):
|
||||
# Should it be documented
|
||||
if definition.get("kind") == "function" and definition.get("static") == "yes":
|
||||
continue
|
||||
|
||||
# Is the definition documented?
|
||||
documented = False
|
||||
for k in ("briefdescription", "detaileddescription", "inbodydescription"):
|
||||
if definition.findall(f"./{k}/"):
|
||||
documented = True
|
||||
break
|
||||
|
||||
# Name
|
||||
d_def = definition.find("./definition")
|
||||
d_nam = definition.find("./name")
|
||||
|
||||
if not sourcefile:
|
||||
l = definition.find("./location")
|
||||
if l is not None:
|
||||
sourcefile = l.get("file")
|
||||
|
||||
if d_def is not None:
|
||||
name = d_def.text
|
||||
elif d_nam is not None:
|
||||
name = d_nam.text
|
||||
else:
|
||||
name = definition.get("id")
|
||||
|
||||
# Aggregate
|
||||
definitions[name] = documented
|
||||
|
||||
if not sourcefile:
|
||||
sourcefile = fullpath
|
||||
|
||||
return (sourcefile, definitions)
|
||||
|
||||
|
||||
def parse(path):
|
||||
index_fp = os.path.join(path, "index.xml")
|
||||
if not os.path.exists(index_fp):
|
||||
FATAL("Documentation not present. Exiting.", index_fp)
|
||||
|
||||
tree = ET.parse(index_fp)
|
||||
|
||||
files = {}
|
||||
for entry in tree.findall("compound"):
|
||||
if entry.get("kind") == "dir":
|
||||
continue
|
||||
|
||||
file_fp = os.path.join(path, f"{entry.get('refid')}.xml")
|
||||
sourcefile, definitions = parse_file(file_fp)
|
||||
|
||||
if definitions != {}:
|
||||
files[sourcefile] = definitions
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def report(files, include_files, summary_only):
|
||||
files_sorted = sorted(files.keys())
|
||||
|
||||
if len(include_files) != 0:
|
||||
files_sorted = list(filter(lambda f: f in include_files, files_sorted))
|
||||
|
||||
if len(files_sorted) == 0:
|
||||
FATAL("No files to report on. Exiting.")
|
||||
|
||||
files_sorted.reverse()
|
||||
|
||||
total_yes = 0
|
||||
total_no = 0
|
||||
|
||||
for f in files_sorted:
|
||||
defs = files[f]
|
||||
|
||||
doc_yes = len([d for d in defs.values() if d])
|
||||
doc_no = len([d for d in defs.values() if not d])
|
||||
doc_per = doc_yes * 100.0 / (doc_yes + doc_no)
|
||||
|
||||
total_yes += doc_yes
|
||||
total_no += doc_no
|
||||
|
||||
if not summary_only:
|
||||
print(f"{int(doc_per):3d}% - {f} - ({doc_yes} of {doc_yes + doc_no})")
|
||||
|
||||
if None in defs:
|
||||
del defs[None]
|
||||
if not summary_only:
|
||||
defs_sorted = sorted(defs.keys())
|
||||
for d in defs_sorted:
|
||||
if not defs[d]:
|
||||
print("\t", d)
|
||||
|
||||
total_all = total_yes + total_no
|
||||
total_percentage = total_yes * 100 / total_all
|
||||
print(
|
||||
f"{"" if summary_only else "\n"}{int(total_percentage)}% API documentation coverage"
|
||||
)
|
||||
return (ns.threshold - total_percentage, 0)[total_percentage > ns.threshold]
|
||||
|
||||
|
||||
def main():
|
||||
# Arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"dir", action="store", help="Path to Doxygen's XML doc directory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"include_files",
|
||||
action="extend",
|
||||
nargs="*",
|
||||
help="List of files to check coverage for (Default: all files)",
|
||||
type=str,
|
||||
default=[],
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-error",
|
||||
action="store_true",
|
||||
help="Do not return error code after execution",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--summary-only",
|
||||
action="store_true",
|
||||
help="Only print the summary of the coverage report, without listing the coverage of each file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--threshold",
|
||||
action="store",
|
||||
help=f"Min acceptable coverage percentage (Default: {ACCEPTABLE_COVERAGE})",
|
||||
default=ACCEPTABLE_COVERAGE,
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--generate-docs",
|
||||
action="store_true",
|
||||
help="Generate Doxygen documentation before checking coverage",
|
||||
)
|
||||
|
||||
global ns
|
||||
ns = parser.parse_args()
|
||||
if not ns:
|
||||
FATAL("ERROR: Couldn't parse parameters")
|
||||
|
||||
if ns.generate_docs:
|
||||
generate_docs()
|
||||
|
||||
# Parse
|
||||
files = parse(ns.dir)
|
||||
|
||||
# Print report
|
||||
err = report(files, ns.include_files, ns.summary_only)
|
||||
if ns.no_error:
|
||||
return
|
||||
|
||||
sys.exit(round(err))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue