| %d | %s | %0.1f ℃ | %s",
i + 1, cfg->vsensors[i].name, st->vtemp[i], other);
}
}
else if (!strncmp(tag, "adcvref", 7)) {
printed = snprintf(insert, insertlen, " | Vref | ADC | %0.4f V",
cfg->adc_vref);
}
else if (!strncmp(tag, "csvstat", 7)) {
printed = csv_stats(insert, insertlen, current_tag_part, next_tag_part);
}
else if (!strncmp(tag, "jsonstat", 8)) {
printed = json_stats(insert, insertlen, current_tag_part, next_tag_part);
}
else if (!strncmp(tag, "refresh", 8)) {
/* generate "random" refresh time for a page, to help spread out the load... */
printed = snprintf(insert, insertlen, "%u", (uint)(30 + ((double)rand() / RAND_MAX) * 30));
}
/* Check if snprintf() output was truncated... */
printed = (printed >= insertlen ? insertlen - 1 : printed);
/* printf("printed=%u\n", printed); */
return printed;
}
================================================
FILE: src/i2c.c
================================================
/* i2c.c
Copyright (C) 2024 Timo Kokkonen
SPDX-License-Identifier: GPL-3.0-or-later
This file is part of FanPico.
FanPico is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FanPico is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FanPico. If not, see .
*/
#include
#include
#include
#include
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/i2c.h"
#include "pico_sensor_lib.h"
#include "fanpico.h"
static bool i2c_bus_active = false;
static i2c_inst_t *i2c_bus = NULL;
static int i2c_temp_sensors = 0;
void scan_i2c_bus()
{
int res;
uint8_t buf[2];
int found = 0;
if (!i2c_bus_active)
return;
printf("Scanning I2C Bus... ");
for (uint addr = 0; addr < 0x80; addr++) {
if (i2c_reserved_address(addr))
continue;
res = i2c_read_timeout_us(i2c_bus, addr, buf, 1, false, 10000);
if (res < 0)
continue;
if (found > 0)
printf(", ");
printf("0x%02x", addr);
found++;
}
printf("\nDevice(s) found: %d\n", found);
}
void display_i2c_status()
{
printf("%d\n", i2c_bus_active ? 1 : 0);
}
void setup_i2c_bus(struct fanpico_config *config)
{
int res;
uint baudrate;
i2c_bus_active = false;
#if SDA_PIN >= 0
if (!SPI_SHARED || !config->spi_active)
i2c_bus_active = true;
#endif
if (!i2c_bus_active) {
log_msg(LOG_INFO, "I2C Bus disabled");
return;
}
log_msg(LOG_INFO, "Initializing I2C Bus..");
i2c_bus = (I2C_HW > 1 ? i2c1 : i2c0);
baudrate = i2c_init(i2c_bus, config->i2c_speed);
baudrate /= 1000;
i2c_sensor_baudrate(baudrate);
log_msg(LOG_INFO, "I2C Bus initialized at %u kHz", baudrate);
gpio_set_function(SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(SDA_PIN);
gpio_pull_up(SCL_PIN);
/* Scan for I2C Temperature Sensors */
for (int i = 0; i < VSENSOR_COUNT; i++) {
struct vsensor_input *v = &config->vsensors[i];
void *ctx = NULL;
if (v->mode != VSMODE_I2C)
continue;
if (v->i2c_type < 1)
continue;
res = i2c_init_sensor(v->i2c_type, i2c_bus, v->i2c_addr, &ctx);
if (res) {
log_msg(LOG_NOTICE, "I2C Device %s (at 0x%02x): failed to initialize: %d",
i2c_sensor_type_str(v->i2c_type), v->i2c_addr, res);
continue;
}
config->i2c_context[i] = ctx;
log_msg(LOG_INFO, "I2C Device %s (at 0x%02x): mapped to vsensor%d",
i2c_sensor_type_str(v->i2c_type), v->i2c_addr, i + 1);
i2c_temp_sensors++;
}
}
int i2c_read_temps(struct fanpico_config *config)
{
static uint step = 0;
static uint sensor = 0;
int wait_time = 0;
float temp = 0.0;
float pressure = 0.0;
float humidity = 0.0;
int res;
if (!i2c_bus_active || i2c_temp_sensors < 1)
return -1;
if (step > 1)
step = 0;
if (step == 0) {
/* Send "Convert Temperature" commands to all sensors */
log_msg(LOG_DEBUG, "Initiate I2C sensors temperature conversions.");
for (int i = 0; i < VSENSOR_COUNT; i++) {
struct vsensor_input *v = &config->vsensors[i];
if (v->mode != VSMODE_I2C)
continue;
if (v->i2c_type < 1 || !config->i2c_context[i])
continue;
res = i2c_start_measurement(config->i2c_context[i]);
if (res >= 0) {
if (res > wait_time)
wait_time = res;
} else {
log_msg(LOG_DEBUG, "vsensor%d: I2C temp conversion fail: %d",
i + 1, res);
}
}
if (wait_time < 1)
wait_time = 15000;
sensor = 0;
step++;
}
else if (step == 1) {
/* Read temperature measurements one sensor at a time... */
if (sensor == 0)
log_msg(LOG_DEBUG, "Initiate I2C sensor measurement readings.");
int i;
for (i = sensor; i < VSENSOR_COUNT; i++) {
struct vsensor_input *v = &config->vsensors[i];
if (v->mode != VSMODE_I2C)
continue;
if (v->i2c_type < 1 || !config->i2c_context[i])
continue;
res = i2c_read_measurement(config->i2c_context[i], &temp, &pressure, &humidity);
if (res == 0) {
if (pressure >= 0.0 || humidity >= 0.0 ) {
log_msg(LOG_DEBUG, "vsensor%d: temp=%0.4fC, pressure=%0.2fhPa, humidity=%0.2f%%",
i + 1, temp, pressure, humidity);
} else {
log_msg(LOG_DEBUG, "vsensor%d: temperature %0.4f C", i + 1, temp);
}
mutex_enter_blocking(config_mutex);
config->vtemp[i] = temp;
config->vpressure[i] = pressure;
config->vhumidity[i] = humidity;
config->vtemp_updated[i] = get_absolute_time();
mutex_exit(config_mutex);
} else {
log_msg(LOG_INFO, "vsensor%d: I2C get temperature failed: %d",
i + 1, res);
}
break;
}
sensor = i + 1;
if (sensor >= VSENSOR_COUNT) {
wait_time = 10000;
step++;
log_msg(LOG_DEBUG, "I2C Temperature measurements complete.");
} else {
wait_time = 50;
}
}
return wait_time;
}
================================================
FILE: src/log.c
================================================
/* log.c
Copyright (C) 2021-2023 Timo Kokkonen
SPDX-License-Identifier: GPL-3.0-or-later
This file is part of FanPico.
FanPico is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FanPico is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FanPico. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "pico/stdlib.h"
#include "pico/mutex.h"
#include "pico/unique_id.h"
#include "pico/util/datetime.h"
#include "hardware/watchdog.h"
#include "b64/cencode.h"
#include "b64/cdecode.h"
#include "fanpico.h"
#ifdef WIFI_SUPPORT
#include "syslog.h"
#endif
int global_debug_level = 0;
int global_log_level = LOG_ERR;
int global_syslog_level = LOG_ERR;
struct log_priority {
uint8_t priority;
const char *name;
};
struct log_facility {
uint8_t facility;
const char *name;
};
const struct log_priority log_priorities[] = {
{ LOG_EMERG, "EMERG" },
{ LOG_ALERT, "ALERT" },
{ LOG_CRIT, "CRIT" },
{ LOG_ERR, "ERR" },
{ LOG_WARNING, "WARNING" },
{ LOG_NOTICE, "NOTICE" },
{ LOG_INFO, "INFO" },
{ LOG_DEBUG, "DEBUG" },
{ 0, NULL }
};
const struct log_facility log_facilities[] = {
{ LOG_KERN, "KERN" },
{ LOG_USER, "USER" },
{ LOG_MAIL, "MAIL" },
{ LOG_DAEMON, "DAEMON" },
{ LOG_AUTH, "AUTH" },
{ LOG_SYSLOG, "SYSLOG" },
{ LOG_LPR, "LPR" },
{ LOG_NEWS, "NEWS" },
{ LOG_UUCP, "UUCP" },
{ LOG_CRON, "CRON" },
{ LOG_AUTHPRIV, "AUTHPRIV" },
{ LOG_FTP, "FTP" },
{ LOG_LOCAL0, "LOCAL0" },
{ LOG_LOCAL1, "LOCAL1" },
{ LOG_LOCAL2, "LOCAL2" },
{ LOG_LOCAL3, "LOCAL3" },
{ LOG_LOCAL4, "LOCAL4" },
{ LOG_LOCAL5, "LOCAL5" },
{ LOG_LOCAL6, "LOCAL6" },
{ LOG_LOCAL7, "LOCAL7" },
{ 0, NULL }
};
int str2log_priority(const char *pri)
{
int i = 0;
if (!pri)
return -2;
while(log_priorities[i].name) {
if (!strcasecmp(log_priorities[i].name, pri))
return log_priorities[i].priority;
i++;
}
return -1;
}
const char* log_priority2str(int pri)
{
int i = 0;
while(log_priorities[i].name) {
if (log_priorities[i].priority == pri)
return log_priorities[i].name;
i++;
}
return NULL;
}
int str2log_facility(const char *facility)
{
int i = 0;
if (!facility)
return -2;
while(log_facilities[i].name) {
if (!strcasecmp(log_facilities[i].name, facility))
return log_facilities[i].facility;
i++;
}
return -1;
}
const char* log_facility2str(int facility)
{
int i = 0;
while(log_facilities[i].name) {
if (log_facilities[i].facility == facility)
return log_facilities[i].name;
i++;
}
return NULL;
}
int get_log_level()
{
return global_log_level;
}
void set_log_level(int level)
{
global_log_level = level;
}
int get_syslog_level()
{
return global_syslog_level;
}
void set_syslog_level(int level)
{
global_syslog_level = level;
}
int get_debug_level()
{
return global_debug_level;
}
void set_debug_level(int level)
{
global_debug_level = level;
}
void log_msg(int priority, const char *format, ...)
{
va_list ap;
char buf[256];
int len;
uint64_t start, end;
uint core = get_core_num();
if ((priority > global_log_level) && (priority > global_syslog_level))
return;
start = to_us_since_boot(get_absolute_time());
va_start(ap, format);
vsnprintf(buf, sizeof(buf), format, ap);
va_end(ap);
if ((len = strlen(buf)) > 0) {
/* If string ends with \n, remove it. */
if (buf[len - 1] == '\n')
buf[len - 1] = 0;
}
if (priority <= global_log_level) {
uint64_t t = to_us_since_boot(get_absolute_time());
printf("[%6llu.%06llu][%u] %s\n", (t / 1000000), (t % 1000000), core, buf);
}
#ifdef WIFI_SUPPORT
if (priority <= global_syslog_level) {
syslog_msg(priority, "%s", buf);
}
#endif
end = to_us_since_boot(get_absolute_time());
if (end - start > 10000) {
#ifndef NDEBUG
printf("log_msg: core%u: %llu (duration=%llu)\n", core, end, end - start);
#endif
}
}
void debug(int debug_level, const char *fmt, ...)
{
va_list ap;
if (debug_level > global_debug_level)
return;
printf("[DEBUG] ");
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
}
/* eof */
================================================
FILE: src/log.h
================================================
/* log.h
Copyright (C) 2022 Timo Kokkonen
SPDX-License-Identifier: GPL-3.0-or-later
This file is part of FanPico.
FanPico is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FanPico is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FanPico. If not, see .
*/
#ifndef _FANPICO_LOG_H_
#define _FANPICO_LOG_H_
/* Syslog Priorities */
#define LOG_EMERG 0
#define LOG_ALERT 1
#define LOG_CRIT 2
#define LOG_ERR 3
#define LOG_WARNING 4
#define LOG_NOTICE 5
#define LOG_INFO 6
#define LOG_DEBUG 7
/* Syslog Facilities */
#define LOG_KERN 0
#define LOG_USER 1
#define LOG_MAIL 2
#define LOG_DAEMON 3
#define LOG_AUTH 4
#define LOG_SYSLOG 5
#define LOG_LPR 6
#define LOG_NEWS 7
#define LOG_UUCP 8
#define LOG_CRON 9
#define LOG_AUTHPRIV 10
#define LOG_FTP 11
#define LOG_LOCAL0 16
#define LOG_LOCAL1 17
#define LOG_LOCAL2 18
#define LOG_LOCAL3 19
#define LOG_LOCAL4 20
#define LOG_LOCAL5 21
#define LOG_LOCAL6 22
#define LOG_LOCAL7 23
#endif /* _FANPICO_LOG_H_ */
================================================
FILE: src/logos/custom.h
================================================
/* custom.h
Copyright (C) 2022-2023 Timo Kokkonen
SPDX-License-Identifier: GPL-3.0-or-later
This file is part of FanPico.
FanPico is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FanPico is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FanPico. If not, see .
*/
extern const uint8_t fanpico_custom_lcd_logo_bmp[]; /* ptr to logo image */
const display_logo_t custom_lcd_logo_entry = {
"custom",
160,
140,
fanpico_custom_lcd_logo_bmp
};
================================================
FILE: src/logos/custom.s
================================================
# custom.s
# Copyright (C) 2021-2023 Timo Kokkonen
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of FanPico.
#
#
# Stub for embedding lcd-logo-custom.bmp in the executable.
#
.global fanpico_custom_lcd_logo_bmp
.global fanpico_custom_lcd_logo_bmp_end
.section .rodata
.p2align 4
fanpico_custom_lcd_logo_bmp:
.incbin "custom.bmp"
fanpico_custom_lcd_logo_bmp_end:
# eof
================================================
FILE: src/logos/default.h
================================================
/* default.h
Copyright (C) 2022-2023 Timo Kokkonen
SPDX-License-Identifier: GPL-3.0-or-later
This file is part of FanPico.
FanPico is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FanPico is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FanPico. If not, see .
*/
extern const uint8_t fanpico_lcd_logo_bmp[]; /* ptr to logo image */
const display_logo_t default_lcd_logo_entry = {
"default",
160,
140,
fanpico_lcd_logo_bmp
};
================================================
FILE: src/logos/default.s
================================================
# default.s
# Copyright (C) 2021-2023 Timo Kokkonen
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of FanPico.
#
#
# Stub for embedding lcd-logo.bmp in the executable.
#
.global fanpico_lcd_logo_bmp
.global fanpico_lcd_logo_bmp_end
.section .rodata
.p2align 4
fanpico_lcd_logo_bmp:
.incbin "default.bmp"
fanpico_lcd_logo_bmp_end:
# eof
================================================
FILE: src/lwip_hooks.h
================================================
#ifndef _LWIP_HOOKS_H
#define _LWIP_HOOKS_H
#include "lwip/arch.h"
#include "lwip/dhcp.h"
#include "lwip/prot/dhcp.h"
void pico_dhcp_option_add_hook(struct netif *netif, struct dhcp *dhcp, u8_t state, struct dhcp_msg *msg,
u8_t msg_type, u16_t *options_len_ptr);
void pico_dhcp_option_parse_hook(struct netif *netif, struct dhcp *dhcp, u8_t state, struct dhcp_msg *msg,
u8_t msg_type, u8_t option, u8_t option_len, struct pbuf *pbuf, u16_t option_value_offset);
#endif /* _LWIP_HOOKS_H */
================================================
FILE: src/lwipopts.h
================================================
#ifndef _LWIPOPTS_H
#define _LWIPOPTS_H
#include
#include "fanpico-compile.h"
extern uint16_t fanpico_http_server_port;
extern uint16_t fanpico_https_server_port;
// Settings for FanPico when using Pico W...
// (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html for details)
// allow override in some examples
#ifndef NO_SYS
#define NO_SYS 1
#endif
// allow override in some examples
#ifndef LWIP_SOCKET
#define LWIP_SOCKET 0
#endif
#if PICO_CYW43_ARCH_POLL
#define MEM_LIBC_MALLOC 1
#else
// MEM_LIBC_MALLOC is incompatible with non polling versions
#define MEM_LIBC_MALLOC 0
#endif
#define MEM_ALIGNMENT 4
#define MEM_SIZE 49152
#define MEM_SANITY_CHECK 1
#define MEM_OVERFLOW_CHECK 1
#define MEMP_NUM_TCP_SEG 64
#define MEMP_NUM_TCP_PCB 32
#define MEMP_NUM_UDP_PCB 8
#define MEMP_NUM_ARP_QUEUE 10
#define MEMP_NUM_SYS_TIMEOUT (LWIP_NUM_SYS_TIMEOUT_INTERNAL + 4)
#define PBUF_POOL_SIZE 24
#define LWIP_ARP 1
#define LWIP_ETHERNET 1
#define LWIP_ICMP 1
#define LWIP_RAW 1
#define TCP_MSS 1460
#define TCP_WND (12 * TCP_MSS)
#define TCP_SND_BUF (8 * TCP_MSS)
#define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS))
#define LWIP_NETIF_STATUS_CALLBACK 1
#define LWIP_NETIF_LINK_CALLBACK 1
#define LWIP_NETIF_HOSTNAME 1
#define LWIP_NETCONN 0
#define MEM_STATS 1
#define SYS_STATS 1
#define MEMP_STATS 1
#define LINK_STATS 0
// #define ETH_PAD_SIZE 2
#define LWIP_CHKSUM_ALGORITHM 3
#define LWIP_DHCP 1
#define LWIP_IPV4 1
#define LWIP_TCP 1
#define LWIP_UDP 1
#define LWIP_DNS 1
#define LWIP_TCP_KEEPALIVE 1
#define LWIP_NETIF_TX_SINGLE_PBUF 1
#define DHCP_DOES_ARP_CHECK 0
#define LWIP_DHCP_DOES_ACD_CHECK 0
#define LWIP_DHCP_GET_NTP_SRV 1
#define SNTP_GET_SERVERS_FROM_DHCP 1
#define SNTP_STARTUP_DELAY 1
#define SNTP_STARTUP_DELAY_FUNC (5000 + LWIP_RAND() % 4000)
#define SNTP_MAX_SERVERS 2
void pico_set_system_time(long int sec);
#define SNTP_SET_SYSTEM_TIME(sec) pico_set_system_time(sec)
#define LWIP_HOOK_FILENAME "lwip_hooks.h"
#define LWIP_HOOK_DHCP_APPEND_OPTIONS pico_dhcp_option_add_hook
#define LWIP_HOOK_DHCP_PARSE_OPTION pico_dhcp_option_parse_hook
#define MQTT_OUTPUT_RINGBUF_SIZE 4096
#define MQTT_REQ_MAX_IN_FLIGHT 32
#define HTTPD_FSDATA_FILE "fanpico_fsdata.c"
#define HTTPD_USE_MEM_POOL 0
#define HTTPD_SERVER_PORT fanpico_http_server_port
#define HTTPD_SERVER_PORT_HTTPS fanpico_https_server_port
#define LWIP_HTTPD_SSI 1
#define LWIP_HTTPD_SSI_RAW 1
#define LWIP_HTTPD_SSI_MULTIPART 1
#define LWIP_HTTPD_SSI_INCLUDE_TAG 0
#define LWIP_HTTPD_SSI_EXTENSIONS ".shtml", ".xml", ".json", ".csv"
#define LWIP_SNMP 1
#define SNMP_LWIP_MIB2 1
#define LWIP_STATS 1
#define MIB2_STATS 1
#define SNMP_MAX_COMMUNITY_STR_LEN 32
#if TLS_SUPPORT
#define HTTPD_ENABLE_HTTPS 1
#define LWIP_ALTCP 1
#define LWIP_ALTCP_TLS 1
#define LWIP_ALTCP_TLS_MBEDTLS 1
#endif
#ifndef NDEBUG
#define LWIP_DEBUG 1
#define LWIP_STATS 1
#define LWIP_STATS_DISPLAY 1
#define LWIP_DEBUG_TIMERNAMES 1
#endif
#define ETHARP_DEBUG LWIP_DBG_OFF
#define NETIF_DEBUG LWIP_DBG_OFF
#define PBUF_DEBUG LWIP_DBG_OFF
#define API_LIB_DEBUG LWIP_DBG_OFF
#define API_MSG_DEBUG LWIP_DBG_OFF
#define SOCKETS_DEBUG LWIP_DBG_OFF
#define ICMP_DEBUG LWIP_DBG_OFF
#define INET_DEBUG LWIP_DBG_OFF
#define IP_DEBUG LWIP_DBG_OFF
#define IP_REASS_DEBUG LWIP_DBG_OFF
#define RAW_DEBUG LWIP_DBG_OFF
#define MEM_DEBUG LWIP_DBG_ON
#define MEMP_DEBUG LWIP_DBG_ON
#define SYS_DEBUG LWIP_DBG_ON
#define TCP_DEBUG LWIP_DBG_OFF
#define TCP_INPUT_DEBUG LWIP_DBG_OFF
#define TCP_OUTPUT_DEBUG LWIP_DBG_OFF
#define TCP_RTO_DEBUG LWIP_DBG_OFF
#define TCP_CWND_DEBUG LWIP_DBG_OFF
#define TCP_WND_DEBUG LWIP_DBG_OFF
#define TCP_FR_DEBUG LWIP_DBG_OFF
#define TCP_QLEN_DEBUG LWIP_DBG_OFF
#define TCP_RST_DEBUG LWIP_DBG_OFF
#define UDP_DEBUG LWIP_DBG_OFF
#define TCPIP_DEBUG LWIP_DBG_OFF
#define PPP_DEBUG LWIP_DBG_OFF
#define SLIP_DEBUG LWIP_DBG_OFF
#define DHCP_DEBUG LWIP_DBG_OFF
#define SNTP_DEBUG LWIP_DBG_OFF
#define HTTPD_DEBUG LWIP_DBG_OFF
#define MQTT_DEBUG LWIP_DBG_ON
#define ALTCP_MBEDTLS_DEBUG LWIP_DBG_ON
#define ALTCP_MBEDTLS_MEM_DEBUG LWIP_DBG_ON
#define ALTCP_MBEDTLS_LIB_DEBUG LWIP_DBG_ON
#define TIMERS_DEBUG LWIP_DBG_OFF
#endif /* __LWIPOPTS_H__ */
================================================
FILE: src/mbedtls_config.h
================================================
/* Workaround for some mbedtls source files using INT_MAX without including limits.h */
#include
#define MBEDTLS_NO_PLATFORM_ENTROPY
#define MBEDTLS_ENTROPY_HARDWARE_ALT
#define MBEDTLS_SSL_OUT_CONTENT_LEN 4096
/* seems like apparent bug (?) in LwIP altcp_tls requires this when MBEDTLS_SSL_OUT_CONTENT_LEN is over 2048 */
#define MBEDTLS_SSL_MAX_FRAGMENT_LENGTH 4096
#define MBEDTLS_ALLOW_PRIVATE_ACCESS
#define MBEDTLS_HAVE_TIME
#define MBEDTLS_PLATFORM_MS_TIME_ALT
#define MBEDTLS_CIPHER_MODE_CBC
#define MBEDTLS_ECP_DP_SECP192R1_ENABLED
#define MBEDTLS_ECP_DP_SECP224R1_ENABLED
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#define MBEDTLS_ECP_DP_SECP384R1_ENABLED
#define MBEDTLS_ECP_DP_SECP521R1_ENABLED
#define MBEDTLS_ECP_DP_SECP192K1_ENABLED
#define MBEDTLS_ECP_DP_SECP224K1_ENABLED
#define MBEDTLS_ECP_DP_SECP256K1_ENABLED
#define MBEDTLS_ECP_DP_BP256R1_ENABLED
#define MBEDTLS_ECP_DP_BP384R1_ENABLED
#define MBEDTLS_ECP_DP_BP512R1_ENABLED
#define MBEDTLS_ECP_DP_CURVE25519_ENABLED
#define MBEDTLS_ECP_NIST_OPTIM
#define MBEDTLS_KEY_EXCHANGE_RSA_ENABLED
#define MBEDTLS_PKCS1_V15
#define MBEDTLS_SHA256_SMALLER
#define MBEDTLS_SHA512_SMALLER
//#define MBEDTLS_SHA512_NO_SHA384
#define MBEDTLS_SSL_SERVER_NAME_INDICATION
#define MBEDTLS_AES_C
#define MBEDTLS_ASN1_PARSE_C
#define MBEDTLS_BIGNUM_C
#define MBEDTLS_CIPHER_C
#define MBEDTLS_CTR_DRBG_C
#define MBEDTLS_ENTROPY_C
#define MBEDTLS_ERROR_C
#define MBEDTLS_MD_C
#define MBEDTLS_MD5_C
#define MBEDTLS_OID_C
#define MBEDTLS_PKCS5_C
#define MBEDTLS_PK_C
#define MBEDTLS_PK_PARSE_C
#define MBEDTLS_PLATFORM_C
#define MBEDTLS_RSA_C
#define MBEDTLS_SHA1_C
#define MBEDTLS_SHA224_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_SHA512_C
#define MBEDTLS_SSL_CLI_C
#define MBEDTLS_SSL_SRV_C
#define MBEDTLS_SSL_TLS_C
#define MBEDTLS_X509_CRT_PARSE_C
#define MBEDTLS_X509_USE_C
#define MBEDTLS_PEM_PARSE_C
#define MBEDTLS_BASE64_C
#define MBEDTLS_AES_FEWER_TABLES
/* TLS 1.2 */
#define MBEDTLS_SSL_PROTO_TLS1_2
#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
#define MBEDTLS_GCM_C
#define MBEDTLS_ECDH_C
#define MBEDTLS_ECP_C
#define MBEDTLS_ECDSA_C
#define MBEDTLS_ASN1_WRITE_C
#ifndef NDEBUG
#define MBEDTLS_DEBUG_C
#endif
================================================
FILE: src/memmap_custom_rp2040.ld
================================================
/* Based on GCC ARM embedded samples.
Defines the following symbols for use by code:
__exidx_start
__exidx_end
__etext
__data_start__
__preinit_array_start
__preinit_array_end
__init_array_start
__init_array_end
__fini_array_start
__fini_array_end
__data_end__
__bss_start__
__bss_end__
__end__
end
__HeapLimit
__StackLimit
__StackTop
__stack (== StackTop)
*/
MEMORY
{
INCLUDE "pico_flash_region.ld"
RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 252k
SCRATCH_X(rwx) : ORIGIN = 0x2003f000, LENGTH = 4k
SCRATCH_Y(rwx) : ORIGIN = 0x20040000, LENGTH = 8k
}
ENTRY(_entry_point)
SECTIONS
{
/* Second stage bootloader is prepended to the image. It must be 256 bytes big
and checksummed. It is usually built by the boot_stage2 target
in the Raspberry Pi Pico SDK
*/
.flash_begin : {
__flash_binary_start = .;
} > FLASH
.boot2 : {
__boot2_start__ = .;
KEEP (*(.boot2))
__boot2_end__ = .;
} > FLASH
ASSERT(__boot2_end__ - __boot2_start__ == 256,
"ERROR: Pico second stage bootloader must be 256 bytes in size")
/* The second stage will always enter the image at the start of .text.
The debugger will use the ELF entry point, which is the _entry_point
symbol if present, otherwise defaults to start of .text.
This can be used to transfer control back to the bootrom on debugger
launches only, to perform proper flash setup.
*/
.text : {
__logical_binary_start = .;
KEEP (*(.vectors))
KEEP (*(.binary_info_header))
__binary_info_header_end = .;
KEEP (*(.embedded_block))
__embedded_block_end = .;
KEEP (*(.reset))
/* TODO revisit this now memset/memcpy/float in ROM */
/* bit of a hack right now to exclude all floating point and time critical (e.g. memset, memcpy) code from
* FLASH ... we will include any thing excluded here in .data below by default */
*(.init)
*(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .text*)
*(.fini)
/* Pull all c'tors into .text */
*crtbegin.o(.ctors)
*crtbegin?.o(.ctors)
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)
*(SORT(.ctors.*))
*(.ctors)
/* Followed by destructors */
*crtbegin.o(.dtors)
*crtbegin?.o(.dtors)
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors)
*(SORT(.dtors.*))
*(.dtors)
. = ALIGN(4);
/* preinit data */
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP(*(SORT(.preinit_array.*)))
KEEP(*(.preinit_array))
PROVIDE_HIDDEN (__preinit_array_end = .);
. = ALIGN(4);
/* init data */
PROVIDE_HIDDEN (__init_array_start = .);
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array))
PROVIDE_HIDDEN (__init_array_end = .);
. = ALIGN(4);
/* finit data */
PROVIDE_HIDDEN (__fini_array_start = .);
*(SORT(.fini_array.*))
*(.fini_array)
PROVIDE_HIDDEN (__fini_array_end = .);
*(.eh_frame*)
. = ALIGN(4);
} > FLASH
.rodata : {
*(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .rodata*)
. = ALIGN(4);
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.flashdata*)))
. = ALIGN(4);
} > FLASH
.ARM.extab :
{
*(.ARM.extab* .gnu.linkonce.armextab.*)
} > FLASH
__exidx_start = .;
.ARM.exidx :
{
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
} > FLASH
__exidx_end = .;
/* Machine inspectable binary information */
. = ALIGN(4);
__binary_info_start = .;
.binary_info :
{
KEEP(*(.binary_info.keep.*))
*(.binary_info.*)
} > FLASH
__binary_info_end = .;
. = ALIGN(4);
.ram_vector_table (NOLOAD): {
*(.ram_vector_table)
} > RAM
.uninitialized_data (NOLOAD): {
. = ALIGN(4);
*(.uninitialized_data*)
} > RAM
.data : {
__data_start__ = .;
*(vtable)
*(.time_critical*)
/* remaining .text and .rodata; i.e. stuff we exclude above because we want it in RAM */
*(.text*)
. = ALIGN(4);
*(.rodata*)
. = ALIGN(4);
*(.data*)
. = ALIGN(4);
*(.after_data.*)
. = ALIGN(4);
/* preinit data */
PROVIDE_HIDDEN (__mutex_array_start = .);
KEEP(*(SORT(.mutex_array.*)))
KEEP(*(.mutex_array))
PROVIDE_HIDDEN (__mutex_array_end = .);
. = ALIGN(4);
*(.jcr)
. = ALIGN(4);
} > RAM AT> FLASH
.tdata : {
. = ALIGN(4);
*(.tdata .tdata.* .gnu.linkonce.td.*)
/* All data end */
__tdata_end = .;
} > RAM AT> FLASH
PROVIDE(__data_end__ = .);
/* __etext is (for backwards compatibility) the name of the .data init source pointer (...) */
__etext = LOADADDR(.data);
.tbss (NOLOAD) : {
. = ALIGN(4);
__bss_start__ = .;
__tls_base = .;
*(.tbss .tbss.* .gnu.linkonce.tb.*)
*(.tcommon)
__tls_end = .;
} > RAM
.bss (NOLOAD) : {
. = ALIGN(4);
__tbss_end = .;
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*)))
*(COMMON)
. = ALIGN(4);
__bss_end__ = .;
} > RAM
.heap (NOLOAD):
{
__end__ = .;
end = __end__;
KEEP(*(.heap*))
/* historically on GCC sbrk was growing past __HeapLimit to __StackLimit, however
to be more compatible, we now set __HeapLimit explicitly to where the end of the heap is */
. = ORIGIN(RAM) + LENGTH(RAM);
__HeapLimit = .;
} > RAM
/* Start and end symbols must be word-aligned */
.scratch_x : {
__scratch_x_start__ = .;
*(.scratch_x.*)
. = ALIGN(4);
__scratch_x_end__ = .;
} > SCRATCH_X AT > FLASH
__scratch_x_source__ = LOADADDR(.scratch_x);
.scratch_y : {
__scratch_y_start__ = .;
*(.scratch_y.*)
. = ALIGN(4);
__scratch_y_end__ = .;
} > SCRATCH_Y AT > FLASH
__scratch_y_source__ = LOADADDR(.scratch_y);
/* .stack*_dummy section doesn't contains any symbols. It is only
* used for linker to calculate size of stack sections, and assign
* values to stack symbols later
*
* stack1 section may be empty/missing if platform_launch_core1 is not used */
/* by default we put core 0 stack at the end of scratch Y, so that if core 1
* stack is not used then all of SCRATCH_X is free.
*/
.stack1_dummy (NOLOAD):
{
*(.stack1*)
} > SCRATCH_X
.stack_dummy (NOLOAD):
{
KEEP(*(.stack*))
} > SCRATCH_Y
.flash_end : {
KEEP(*(.embedded_end_block*))
PROVIDE(__flash_binary_end = .);
} > FLASH
/* stack limit is poorly named, but historically is maximum heap ptr */
__StackLimit = ORIGIN(RAM) + LENGTH(RAM);
__StackOneTop = ORIGIN(SCRATCH_X) + LENGTH(SCRATCH_X);
__StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y);
__StackOneBottom = __StackOneTop - SIZEOF(.stack1_dummy);
__StackBottom = __StackTop - SIZEOF(.stack_dummy);
PROVIDE(__stack = __StackTop);
/* picolibc and LLVM */
PROVIDE (__heap_start = __end__);
PROVIDE (__heap_end = __HeapLimit);
PROVIDE( __tls_align = MAX(ALIGNOF(.tdata), ALIGNOF(.tbss)) );
PROVIDE( __tls_size_align = (__tls_size + __tls_align - 1) & ~(__tls_align - 1));
PROVIDE( __arm32_tls_tcb_offset = MAX(8, __tls_align) );
/* llvm-libc */
PROVIDE (_end = __end__);
PROVIDE (__llvm_libc_heap_limit = __HeapLimit);
/* Check if data + heap + stack exceeds RAM limit */
ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed")
ASSERT( __binary_info_header_end - __logical_binary_start <= 256, "Binary info must be in first 256 bytes of the binary")
/* todo assert on extra code */
}
================================================
FILE: src/memmap_custom_rp2350.ld
================================================
/* Based on GCC ARM embedded samples.
Defines the following symbols for use by code:
__exidx_start
__exidx_end
__etext
__data_start__
__preinit_array_start
__preinit_array_end
__init_array_start
__init_array_end
__fini_array_start
__fini_array_end
__data_end__
__bss_start__
__bss_end__
__end__
end
__HeapLimit
__StackLimit
__StackTop
__stack (== StackTop)
*/
MEMORY
{
INCLUDE "pico_flash_region.ld"
RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 508k
SCRATCH_X(rwx) : ORIGIN = 0x2007f000, LENGTH = 4k
SCRATCH_Y(rwx) : ORIGIN = 0x20080000, LENGTH = 8k
}
ENTRY(_entry_point)
SECTIONS
{
.flash_begin : {
__flash_binary_start = .;
} > FLASH
/* The bootrom will enter the image at the point indicated in your
IMAGE_DEF, which is usually the reset handler of your vector table.
The debugger will use the ELF entry point, which is the _entry_point
symbol, and in our case is *different from the bootrom's entry point.*
This is used to go back through the bootrom on debugger launches only,
to perform the same initial flash setup that would be performed on a
cold boot.
*/
.text : {
__logical_binary_start = .;
KEEP (*(.vectors))
KEEP (*(.binary_info_header))
__binary_info_header_end = .;
KEEP (*(.embedded_block))
__embedded_block_end = .;
KEEP (*(.reset))
/* TODO revisit this now memset/memcpy/float in ROM */
/* bit of a hack right now to exclude all floating point and time critical (e.g. memset, memcpy) code from
* FLASH ... we will include any thing excluded here in .data below by default */
*(.init)
*libgcc.a:cmse_nonsecure_call.o
*(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .text*)
*(.fini)
/* Pull all c'tors into .text */
*crtbegin.o(.ctors)
*crtbegin?.o(.ctors)
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)
*(SORT(.ctors.*))
*(.ctors)
/* Followed by destructors */
*crtbegin.o(.dtors)
*crtbegin?.o(.dtors)
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors)
*(SORT(.dtors.*))
*(.dtors)
. = ALIGN(4);
/* preinit data */
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP(*(SORT(.preinit_array.*)))
KEEP(*(.preinit_array))
PROVIDE_HIDDEN (__preinit_array_end = .);
. = ALIGN(4);
/* init data */
PROVIDE_HIDDEN (__init_array_start = .);
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array))
PROVIDE_HIDDEN (__init_array_end = .);
. = ALIGN(4);
/* finit data */
PROVIDE_HIDDEN (__fini_array_start = .);
*(SORT(.fini_array.*))
*(.fini_array)
PROVIDE_HIDDEN (__fini_array_end = .);
*(.eh_frame*)
. = ALIGN(4);
} > FLASH
/* Note the boot2 section is optional, and should be discarded if there is
no reference to it *inside* the binary, as it is not called by the
bootrom. (The bootrom performs a simple best-effort XIP setup and
leaves it to the binary to do anything more sophisticated.) However
there is still a size limit of 256 bytes, to ensure the boot2 can be
stored in boot RAM.
Really this is a "XIP setup function" -- the name boot2 is historic and
refers to its dual-purpose on RP2040, where it also handled vectoring
from the bootrom into the user image.
*/
.boot2 : {
__boot2_start__ = .;
*(.boot2)
__boot2_end__ = .;
} > FLASH
ASSERT(__boot2_end__ - __boot2_start__ <= 256,
"ERROR: Pico second stage bootloader must be no more than 256 bytes in size")
.rodata : {
*(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .rodata*)
*(.srodata*)
. = ALIGN(4);
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.flashdata*)))
. = ALIGN(4);
} > FLASH
.ARM.extab :
{
*(.ARM.extab* .gnu.linkonce.armextab.*)
} > FLASH
__exidx_start = .;
.ARM.exidx :
{
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
} > FLASH
__exidx_end = .;
/* Machine inspectable binary information */
. = ALIGN(4);
__binary_info_start = .;
.binary_info :
{
KEEP(*(.binary_info.keep.*))
*(.binary_info.*)
} > FLASH
__binary_info_end = .;
. = ALIGN(4);
.ram_vector_table (NOLOAD): {
*(.ram_vector_table)
} > RAM
.uninitialized_data (NOLOAD): {
. = ALIGN(4);
*(.uninitialized_data*)
} > RAM
.data : {
__data_start__ = .;
*(vtable)
*(.time_critical*)
/* remaining .text and .rodata; i.e. stuff we exclude above because we want it in RAM */
*(.text*)
. = ALIGN(4);
*(.rodata*)
. = ALIGN(4);
*(.data*)
*(.sdata*)
. = ALIGN(4);
*(.after_data.*)
. = ALIGN(4);
/* preinit data */
PROVIDE_HIDDEN (__mutex_array_start = .);
KEEP(*(SORT(.mutex_array.*)))
KEEP(*(.mutex_array))
PROVIDE_HIDDEN (__mutex_array_end = .);
*(.jcr)
. = ALIGN(4);
} > RAM AT> FLASH
.tdata : {
. = ALIGN(4);
*(.tdata .tdata.* .gnu.linkonce.td.*)
/* All data end */
__tdata_end = .;
} > RAM AT> FLASH
PROVIDE(__data_end__ = .);
/* __etext is (for backwards compatibility) the name of the .data init source pointer (...) */
__etext = LOADADDR(.data);
.tbss (NOLOAD) : {
. = ALIGN(4);
__bss_start__ = .;
__tls_base = .;
*(.tbss .tbss.* .gnu.linkonce.tb.*)
*(.tcommon)
__tls_end = .;
} > RAM
.bss (NOLOAD) : {
. = ALIGN(4);
__tbss_end = .;
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*)))
*(COMMON)
PROVIDE(__global_pointer$ = . + 2K);
*(.sbss*)
. = ALIGN(4);
__bss_end__ = .;
} > RAM
.heap (NOLOAD):
{
__end__ = .;
end = __end__;
KEEP(*(.heap*))
/* historically on GCC sbrk was growing past __HeapLimit to __StackLimit, however
to be more compatible, we now set __HeapLimit explicitly to where the end of the heap is */
. = ORIGIN(RAM) + LENGTH(RAM);
__HeapLimit = .;
} > RAM
/* Start and end symbols must be word-aligned */
.scratch_x : {
__scratch_x_start__ = .;
*(.scratch_x.*)
. = ALIGN(4);
__scratch_x_end__ = .;
} > SCRATCH_X AT > FLASH
__scratch_x_source__ = LOADADDR(.scratch_x);
.scratch_y : {
__scratch_y_start__ = .;
*(.scratch_y.*)
. = ALIGN(4);
__scratch_y_end__ = .;
} > SCRATCH_Y AT > FLASH
__scratch_y_source__ = LOADADDR(.scratch_y);
/* .stack*_dummy section doesn't contains any symbols. It is only
* used for linker to calculate size of stack sections, and assign
* values to stack symbols later
*
* stack1 section may be empty/missing if platform_launch_core1 is not used */
/* by default we put core 0 stack at the end of scratch Y, so that if core 1
* stack is not used then all of SCRATCH_X is free.
*/
.stack1_dummy (NOLOAD):
{
*(.stack1*)
} > SCRATCH_X
.stack_dummy (NOLOAD):
{
KEEP(*(.stack*))
} > SCRATCH_Y
.flash_end : {
KEEP(*(.embedded_end_block*))
PROVIDE(__flash_binary_end = .);
} > FLASH =0xaa
/* stack limit is poorly named, but historically is maximum heap ptr */
__StackLimit = ORIGIN(RAM) + LENGTH(RAM);
__StackOneTop = ORIGIN(SCRATCH_X) + LENGTH(SCRATCH_X);
__StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y);
__StackOneBottom = __StackOneTop - SIZEOF(.stack1_dummy);
__StackBottom = __StackTop - SIZEOF(.stack_dummy);
PROVIDE(__stack = __StackTop);
/* picolibc and LLVM */
PROVIDE (__heap_start = __end__);
PROVIDE (__heap_end = __HeapLimit);
PROVIDE( __tls_align = MAX(ALIGNOF(.tdata), ALIGNOF(.tbss)) );
PROVIDE( __tls_size_align = (__tls_size + __tls_align - 1) & ~(__tls_align - 1));
PROVIDE( __arm32_tls_tcb_offset = MAX(8, __tls_align) );
/* llvm-libc */
PROVIDE (_end = __end__);
PROVIDE (__llvm_libc_heap_limit = __HeapLimit);
/* Check if data + heap + stack exceeds RAM limit */
ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed")
ASSERT( __binary_info_header_end - __logical_binary_start <= 1024, "Binary info must be in first 1024 bytes of the binary")
ASSERT( __embedded_block_end - __logical_binary_start <= 4096, "Embedded block must be in first 4096 bytes of the binary")
/* todo assert on extra code */
}
================================================
FILE: src/memtest.c
================================================
/* memtest.c
Copyright (C) 2025 Timo Kokkonen
SPDX-License-Identifier: GPL-3.0-or-later
This file is part of FanPico.
FanPico is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FanPico is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FanPico. If not, see .
*/
#include
#include
#include
#include "pico/stdlib.h"
void* walking_mem_test(void *heap, size_t size)
{
volatile uint32_t *t = (uint32_t*)heap;
uint32_t len = size / sizeof(uint32_t);
uint64_t t_start, t_end, speed;
printf("Walking 1's test: ");
t_start = time_us_64();
for (uint32_t bit = 0; bit < 32; bit++) {
/* Write bits */
for (uint32_t i = 0; i < len; i++)
t[i] = 1 << ((bit + i) % 32);
/* Read bits */
for (uint32_t i = 0; i < len; i++)
if (t[i] != 1 << ((bit + i) % 32)) {
printf(" ERROR: %p (%lu)\n", &t[i], i);
return (void*)&t[i];
}
printf(".");
}
t_end = time_us_64();
speed = ((uint64_t)size * 1000000) / (t_end - t_start);
printf(" OK (%lld KB/s)\n", speed / 1024);
return NULL;
}
int simple_speed_mem_test(void *heap, size_t size, bool readonly)
{
volatile uint32_t *t = (uint32_t*)heap;
uint32_t len = size / sizeof(uint32_t);
uint64_t start, end, speed;
volatile uint64_t read;
if (!readonly) {
printf("Testing write speed (32bit)...");
start = time_us_64();
for (int i = 0; i < len; i ++) {
t[i] = 0xdeadbeef;
}
end = time_us_64();
speed = ((uint64_t)size * 1000000) / (end - start);
printf(" %llu KB/s\n", speed / 1024);
}
printf("Testing read speed (32bit)....");
start = time_us_64();
for (int i = 0; i < len; i ++) {
read = t[i];
}
end = time_us_64();
if (!readonly && read != 0xdeadbeef) {
printf(" (error)");
}
speed = ((uint64_t)size * 1000000) / (end - start);
printf(" %llu KB/s\n", speed / 1024);
if (!readonly) {
printf("memset() speed,...............");
start = time_us_64();
memset(heap, 0, size);
end = time_us_64();
speed = ((uint64_t)size * 1000000) / (end - start);
printf(" %llu KB/s\n", speed / 1024);
}
return 0;
}
================================================
FILE: src/memtest.h
================================================
/* memtest.h
Copyright (C) 2025 Timo Kokkonen
SPDX-License-Identifier: GPL-3.0-or-later
This file is part of FanPico.
FanPico is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FanPico is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FanPico. If not, see .
*/
#ifndef FANPICO_MEMTEST_H
#define FANPICO_MEMTEST_H 1
int walking_mem_test(void *heap, size_t size);
int simple_speed_mem_test(void *heap, size_t size, bool readonly);
#endif /* FANPICO_MEMTEST_H */
================================================
FILE: src/mqtt.c
================================================
/* mqtt.c
Copyright (C) 2023-2024 Timo Kokkonen
SPDX-License-Identifier: GPL-3.0-or-later
This file is part of FanPico.
FanPico is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FanPico is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FanPico. If not, see .
*/
#include
#include
#include
#include
#include "pico/stdlib.h"
#include "cJSON.h"
#ifdef LIB_PICO_CYW43_ARCH
#include "pico/cyw43_arch.h"
#include "lwip/dns.h"
#include "lwip/apps/mqtt.h"
#if TLS_SUPPORT
#include "lwip/altcp_tls.h"
#endif
#endif
#include "fanpico.h"
#ifdef WIFI_SUPPORT
#define MQTT_CMD_MAX_LEN 100
mqtt_client_t *mqtt_client = NULL;
ip_addr_t mqtt_server_ip = IPADDR4_INIT_BYTES(0, 0, 0, 0);
u16_t mqtt_server_port = 0;
int incoming_topic = 0;
int mqtt_qos = 1;
char mqtt_ha_birth_topic[64 + 1];
char mqtt_ha_base_topic[64 + 1];
char mqtt_scpi_cmd[MQTT_CMD_MAX_LEN];
bool mqtt_scpi_cmd_queued = false;
absolute_time_t ABSOLUTE_TIME_INITIALIZED_VAR(t_mqtt_disconnect, 0);
absolute_time_t ABSOLUTE_TIME_INITIALIZED_VAR(t_mqtt_ha_discovery, 0);
u16_t mqtt_reconnect = 0;
u16_t mqtt_ha_discovery = 0;
static void mqtt_connect(mqtt_client_t *client);
static void mqtt_pub_request_cb(void *arg, err_t result)
{
const char *topic = (const char*)arg;
if (!topic)
topic = "NULL";
if (result == ERR_OK) {
log_msg(LOG_DEBUG, "MQTT publish successful (%s)", topic);
}
else if (result == ERR_TIMEOUT) {
log_msg(LOG_NOTICE, "MQTT publish failed: timeout (%s)", topic);
}
else {
log_msg(LOG_NOTICE, "MQTT publish failed: %d (%s)", result, topic);
}
}
static int mqtt_publish_message(const char *topic, const char *buf, u16_t buf_len,
u8_t qos, u8_t retain, const char *arg)
{
if (!topic || !buf || buf_len == 0)
return -1;
if (!mqtt_client)
return -2;
/* Check that MQTT Client is connected */
cyw43_arch_lwip_begin();
u8_t connected = mqtt_client_is_connected(mqtt_client);
cyw43_arch_lwip_end();
if (!connected)
return -3;
log_msg(LOG_INFO, "MQTT publish to %s: %u bytes.", topic, buf_len);
/* Publish message to a MQTT topic */
cyw43_arch_lwip_begin();
err_t err = mqtt_publish(mqtt_client, topic, buf, buf_len,
qos, retain, mqtt_pub_request_cb, (void*)arg);
cyw43_arch_lwip_end();
if (err != ERR_OK) {
log_msg(LOG_NOTICE, "mqtt_publish_message(): failed %d (topic=%s, buf_len=%u)",
err, topic, buf_len);
}
return err;
}
static char* json_response_message(const char *cmd, int result, const char *msg)
{
char *buf;
cJSON *json;
if (!(json = cJSON_CreateObject()))
goto panic;
cJSON_AddItemToObject(json, "command", cJSON_CreateString(cmd));
cJSON_AddItemToObject(json, "result", cJSON_CreateString(result == 0 ? "OK" : "ERROR"));
cJSON_AddItemToObject(json, "message", cJSON_CreateString(msg));
if (!(buf = cJSON_Print(json)))
goto panic;
cJSON_Delete(json);
return buf;
panic:
if (json)
cJSON_Delete(json);
return NULL;
}
static void send_mqtt_command_response(const char *cmd, int result, const char *msg)
{
char *buf = NULL;
if (!cmd || !msg || !mqtt_client || strlen(cfg->mqtt_resp_topic) < 1)
return;
/* Generate status message */
if (!(buf = json_response_message(cmd, result, msg))) {
log_msg(LOG_WARNING,"json_response_message(): failed");
return;
}
mqtt_publish_message(cfg->mqtt_resp_topic, buf, strlen(buf), mqtt_qos, 0,
cfg->mqtt_resp_topic);
free(buf);
}
static void mqtt_incoming_publish_cb(void *arg, const char *topic, u32_t tot_len)
{
log_msg(LOG_DEBUG, "MQTT incoming publish at topic %s with total length %u",
topic, (unsigned int)tot_len);
if (!strncmp(topic, cfg->mqtt_cmd_topic, strlen(cfg->mqtt_cmd_topic) + 1)) {
incoming_topic = 1;
} else if (strlen(mqtt_ha_birth_topic) > 0) {
if (!strncmp(topic, mqtt_ha_birth_topic, strlen(mqtt_ha_birth_topic) + 1)) {
incoming_topic = 2;
}
} else {
log_msg(LOG_DEBUG, "Ignoring publish on unknown topic: %s", topic);
incoming_topic = 0;
}
}
static void incoming_fanpico_cmd(const u8_t *data, u16_t len)
{
char cmd[MQTT_CMD_MAX_LEN];
const u8_t *end, *start;
int l;
/* Check for command prefix, if found skip past prefix */
if ((start = memmem(data, len, "CMD:", 4))) {
start += 4;
l = len - (start - data);
if (l < 1)
return;
} else {
start = data;
l = len;
}
/* Check for command suffix, if found ignore anything past it */
if (!(end = memchr(start, ';', l)))
end = start + l;
if ((l = end - start) < 1)
return;
if (l >= sizeof(cmd))
l = sizeof(cmd) - 1;
memcpy(cmd, start, l);
cmd[l] = 0;
/* Check if should be command allowed */
if (!cfg->mqtt_allow_scpi) {
if (strncasecmp(cmd, "WRITE:", 6)) {
log_msg(LOG_NOTICE, "MQTT SCPI commands not allowed: '%s'", cmd);
return;
}
}
if (mqtt_scpi_cmd_queued) {
log_msg(LOG_NOTICE, "MQTT SCPI command queue full: '%s'", cmd);
send_mqtt_command_response(cmd, 1, "SCPI command queue full");
} else {
log_msg(LOG_NOTICE, "MQTT SCPI command queued: '%s'", cmd);
strncopy(mqtt_scpi_cmd, cmd, sizeof(mqtt_scpi_cmd));
mqtt_scpi_cmd_queued = true;
}
}
static void incoming_ha_status(const u8_t *data, u16_t len)
{
/* Handle incoming Home Assistant status messages (online/offline) */
log_msg(LOG_INFO, "Home Assistant status message received: %d", len);
if (memmem(data, len, "online", 6)) {
log_msg(LOG_INFO, "Schedule resending HA MQTT Discovery messages");
mqtt_ha_discovery = 1;
t_mqtt_ha_discovery = get_absolute_time();
}
}
static void mqtt_incoming_data_cb(void *arg, const u8_t *data, u16_t len, u8_t flags)
{
log_msg(LOG_DEBUG, "MQTT incoming publish payload with length %d, flags %u\n",
len, (unsigned int)flags);
if (len < 1 || !data)
return;
if (incoming_topic == 1) {
incoming_fanpico_cmd(data, len);
}
else if (incoming_topic == 2) {
incoming_ha_status(data, len);
}
}
static void mqtt_sub_request_cb(void *arg, err_t result)
{
if (result != ERR_OK) {
log_msg(LOG_WARNING, "MQTT failed to subscribe command topic: %d", result);
}
}
static void mqtt_ha_sub_request_cb(void *arg, err_t result)
{
if (result != ERR_OK) {
log_msg(LOG_WARNING, "MQTT failed to subscribe HA Birth topic: %d", result);
}
}
static void mqtt_connection_cb(mqtt_client_t *client, void *arg, mqtt_connection_status_t status)
{
t_mqtt_disconnect = get_absolute_time();
if (status == MQTT_CONNECT_ACCEPTED) {
log_msg(LOG_INFO, "MQTT connected to %s:%u", ipaddr_ntoa(&mqtt_server_ip),
mqtt_server_port);
mqtt_set_inpub_callback(client, mqtt_incoming_publish_cb, mqtt_incoming_data_cb, arg);
if (strlen(cfg->mqtt_cmd_topic) > 0) {
/* Subscribe to command topic */
log_msg(LOG_INFO, "MQTT subscribe to command topic: %s", cfg->mqtt_cmd_topic);
err_t err = mqtt_subscribe(client, cfg->mqtt_cmd_topic, 1,
mqtt_sub_request_cb, arg);
if (err != ERR_OK) {
log_msg(LOG_WARNING, "MQTT subscribe failed: %d", err);
}
}
if (strlen(cfg->mqtt_ha_discovery_prefix) > 0) {
/* Subscribe to Home Assistant Birth topic */
snprintf(mqtt_ha_birth_topic, sizeof(mqtt_ha_birth_topic), "%s/status",
cfg->mqtt_ha_discovery_prefix);
log_msg(LOG_INFO, "MQTT subscribe to HA Birth topic: %s", mqtt_ha_birth_topic);
err_t err = mqtt_subscribe(client, mqtt_ha_birth_topic, 1, mqtt_ha_sub_request_cb, arg);
if (err != ERR_OK) {
log_msg(LOG_WARNING, "MQTT subscribe failed: %d", err);
}
snprintf(mqtt_ha_base_topic, sizeof(mqtt_ha_base_topic), "%s/sensor/fanpico_%s",
cfg->mqtt_ha_discovery_prefix, pico_serial_str());
mqtt_ha_discovery = 5;
t_mqtt_ha_discovery = get_absolute_time();
}
}
else if (status == MQTT_CONNECT_DISCONNECTED) {
log_msg(LOG_WARNING, "MQTT disconnected from %s", ipaddr_ntoa(&mqtt_server_ip));
mqtt_reconnect = 15;
}
else if (status == MQTT_CONNECT_TIMEOUT) {
log_msg(LOG_WARNING, "MQTT connect: timeout (%s)", ipaddr_ntoa(&mqtt_server_ip));
mqtt_reconnect = 30;
}
else if (status == MQTT_CONNECT_REFUSED_USERNAME_PASS) {
log_msg(LOG_WARNING, "MQTT connect: login failure (%s)", ipaddr_ntoa(&mqtt_server_ip));
}
else if (status == MQTT_CONNECT_REFUSED_NOT_AUTHORIZED_) {
log_msg(LOG_WARNING, "MQTT connect: not authorized (%s)", ipaddr_ntoa(&mqtt_server_ip));
mqtt_reconnect = 180;
}
else {
log_msg(LOG_WARNING, "MQTT connect: refused (status=%d) (%s)", status, ipaddr_ntoa(&mqtt_server_ip));
mqtt_reconnect = 90;
}
}
static void mqtt_dns_resolve_cb(const char *name, const ip_addr_t *ipaddr, void *arg)
{
if (ipaddr && ipaddr->addr) {
log_msg(LOG_INFO, "DNS resolved: %s -> %s\n", name, ipaddr_ntoa(ipaddr));
ip_addr_set(&mqtt_server_ip, ipaddr);
mqtt_connect(mqtt_client);
} else {
log_msg(LOG_WARNING, "Failed to resolve MQTT server name: %s", name);
t_mqtt_disconnect = get_absolute_time();
mqtt_reconnect = 30;
}
}
static void mqtt_connect(mqtt_client_t *client)
{
#if TLS_SUPPORT
static struct altcp_tls_config *tlsconfig = NULL;
#endif
struct mqtt_connect_client_info_t ci;
char client_id[32];
uint16_t port = MQTT_PORT;
err_t err;
if (!client)
return;
mqtt_reconnect = 0;
mqtt_ha_discovery = 0;
/* Resolve domain name */
cyw43_arch_lwip_begin();
err = dns_gethostbyname(cfg->mqtt_server, &mqtt_server_ip, mqtt_dns_resolve_cb, NULL);
cyw43_arch_lwip_end();
if (err != ERR_OK) {
if (err == ERR_INPROGRESS)
log_msg(LOG_INFO, "Resolving DNS name: %s\n", cfg->mqtt_server);
else
log_msg(LOG_WARNING, "Failed to resolve MQTT server name: %s (%d)" ,
cfg->mqtt_server, err);
return;
}
/* Setup client connect info */
memset(&ci, 0, sizeof(ci));
snprintf(client_id, sizeof(client_id), "fanpico-%s_%s", FANPICO_BOARD, pico_serial_str());
ci.client_id = client_id;
ci.client_user = cfg->mqtt_user;
ci.client_pass = cfg->mqtt_pass;
ci.keep_alive = 60;
ci.will_topic = NULL;
ci.will_msg = NULL;
ci.will_retain = 0;
ci.will_qos = 0;
#if TLS_SUPPORT
if (cfg->mqtt_tls) {
if (!tlsconfig) {
cyw43_arch_lwip_begin();
tlsconfig = altcp_tls_create_config_client(NULL, 0);
cyw43_arch_lwip_end();
if (!tlsconfig)
log_msg(LOG_WARNING, "altcp_tls_create_config_client(): failed");
}
ci.tls_config = tlsconfig;
port = MQTT_TLS_PORT;
}
#endif
if (cfg->mqtt_port > 0)
port = cfg->mqtt_port;
mqtt_server_port = port;
/* Connect to MQTT Server */
log_msg(LOG_NOTICE, "MQTT Connecting to %s:%u%s", cfg->mqtt_server, mqtt_server_port,
cfg->mqtt_tls ? " (TLS)" : "");
cyw43_arch_lwip_begin();
err = mqtt_client_connect(mqtt_client, &mqtt_server_ip, mqtt_server_port,
mqtt_connection_cb, 0, &ci);
cyw43_arch_lwip_end();
if (err != ERR_OK) {
log_msg(LOG_WARNING,"mqtt_client_connect(): failed %d", err);
if (err == ERR_RTE) {
t_mqtt_disconnect = get_absolute_time();
mqtt_reconnect = 30;
}
}
}
void fanpico_setup_mqtt_client()
{
if (mqtt_client)
return;
ip_addr_set_zero(&mqtt_server_ip);
mqtt_reconnect = 0;
mqtt_ha_discovery = 0;
mqtt_ha_birth_topic[0] = 0;
mqtt_ha_base_topic[0] = 0;
mqtt_scpi_cmd[0] = 0;
cyw43_arch_lwip_begin();
mqtt_client = mqtt_client_new();
cyw43_arch_lwip_end();
if (!mqtt_client) {
log_msg(LOG_WARNING,"mqtt_client_new(): failed");
return;
}
mqtt_connect(mqtt_client);
}
int fanpico_mqtt_client_active()
{
return (mqtt_client == NULL ? 0 : 1);
}
void fanpico_mqtt_reconnect()
{
if (mqtt_reconnect == 0)
return;
if (time_passed(&t_mqtt_disconnect, mqtt_reconnect * 1000)) {
log_msg(LOG_INFO, "MQTT attempt reconnecting to server");
cyw43_arch_lwip_begin();
u8_t connected = mqtt_client_is_connected(mqtt_client);
cyw43_arch_lwip_end();
if (connected) {
log_msg(LOG_INFO, "MQTT attempt disconnecting existing connection");
cyw43_arch_lwip_begin();
mqtt_disconnect(mqtt_client);
cyw43_arch_lwip_end();
}
mqtt_connect(mqtt_client);
}
}
static char* json_ha_discovery_message(const char *type, int idx, int count)
{
char *buf;
cJSON *json, *d, *id;
char tmp[100];
if (!(json = cJSON_CreateObject()))
return NULL;
if (!strncmp(type, "sensor", 6)) {
snprintf(tmp, sizeof(tmp), "Sensor: %s", cfg->sensors[idx - 1].name);
cJSON_AddItemToObject(json, "name", cJSON_CreateString(tmp));
cJSON_AddItemToObject(json, "device_class", cJSON_CreateString("temperature"));
cJSON_AddItemToObject(json, "unit_of_measurement", cJSON_CreateString("°C"));
}
else if (!strncmp(type, "vtemp", 5)) {
snprintf(tmp, sizeof(tmp), "Sensor: %s", cfg->vsensors[idx - 1].name);
cJSON_AddItemToObject(json, "name", cJSON_CreateString(tmp));
cJSON_AddItemToObject(json, "device_class", cJSON_CreateString("temperature"));
cJSON_AddItemToObject(json, "unit_of_measurement", cJSON_CreateString("°C"));
}
else if (!strncmp(type, "vhumidity", 9)) {
snprintf(tmp, sizeof(tmp), "Sensor: %s", cfg->vsensors[idx - 1].name);
cJSON_AddItemToObject(json, "name", cJSON_CreateString(tmp));
cJSON_AddItemToObject(json, "device_class", cJSON_CreateString("humidity"));
cJSON_AddItemToObject(json, "unit_of_measurement", cJSON_CreateString("%"));
}
else if (!strncmp(type, "vpressure", 9)) {
snprintf(tmp, sizeof(tmp), "Sensor: %s", cfg->vsensors[idx - 1].name);
cJSON_AddItemToObject(json, "name", cJSON_CreateString(tmp));
cJSON_AddItemToObject(json, "device_class", cJSON_CreateString("pressure"));
cJSON_AddItemToObject(json, "unit_of_measurement", cJSON_CreateString("hPa"));
}
else if (!strncmp(type, "fanrpm", 6)) {
snprintf(tmp, sizeof(tmp), "Fan: %s", cfg->fans[idx - 1].name);
cJSON_AddItemToObject(json, "name", cJSON_CreateString(tmp));
cJSON_AddItemToObject(json, "icon", cJSON_CreateString("mdi:fan"));
cJSON_AddItemToObject(json, "unit_of_measurement", cJSON_CreateString("RPM"));
}
else if (!strncmp(type, "fanpwm", 6)) {
snprintf(tmp, sizeof(tmp), "Fan: %s", cfg->fans[idx - 1].name);
cJSON_AddItemToObject(json, "name", cJSON_CreateString(tmp));
cJSON_AddItemToObject(json, "icon", cJSON_CreateString("mdi:speedometer"));
cJSON_AddItemToObject(json, "unit_of_measurement", cJSON_CreateString("%"));
}
else if (!strncmp(type, "mbfanrpm", 8)) {
snprintf(tmp, sizeof(tmp), "MB Fan: %s", cfg->mbfans[idx - 1].name);
cJSON_AddItemToObject(json, "name", cJSON_CreateString(tmp));
cJSON_AddItemToObject(json, "icon", cJSON_CreateString("mdi:fan"));
cJSON_AddItemToObject(json, "unit_of_measurement", cJSON_CreateString("RPM"));
}
else if (!strncmp(type, "mbfanpwm", 8)) {
snprintf(tmp, sizeof(tmp), "MB Fan: %s", cfg->mbfans[idx - 1].name);
cJSON_AddItemToObject(json, "name", cJSON_CreateString(tmp));
cJSON_AddItemToObject(json, "icon", cJSON_CreateString("mdi:speedometer"));
cJSON_AddItemToObject(json, "unit_of_measurement", cJSON_CreateString("%"));
}
else {
goto panic;
}
snprintf(tmp, sizeof(tmp), "{{ value_json.%s%d }}", type, idx);
cJSON_AddItemToObject(json, "value_template", cJSON_CreateString(tmp));
snprintf(tmp, sizeof(tmp), "%s/state", mqtt_ha_base_topic);
cJSON_AddItemToObject(json, "state_topic", cJSON_CreateString(tmp));
snprintf(tmp, sizeof(tmp), "fanpico_%s_%s%d", pico_serial_str(), type, idx);
cJSON_AddItemToObject(json, "unique_id", cJSON_CreateString(tmp));
if (!(d = cJSON_CreateObject()))
goto panic;
if (!(id = cJSON_CreateArray()))
goto panic;
snprintf(tmp, sizeof(tmp), "fanpico_%s", pico_serial_str());
cJSON_AddItemToArray(id, cJSON_CreateString(tmp));
cJSON_AddItemToObject(d, "identifiers", id);
if (count == 0) {
cJSON_AddItemToObject(d, "name", cJSON_CreateString(cfg->name));
cJSON_AddItemToObject(d, "manufacturer", cJSON_CreateString("TJKO Industries"));
cJSON_AddItemToObject(d, "model", cJSON_CreateString("FanPico"));
cJSON_AddItemToObject(d, "model_id", cJSON_CreateString(FANPICO_MODEL));
cJSON_AddItemToObject(d, "serial_number", cJSON_CreateString(pico_serial_str()));
cJSON_AddItemToObject(d, "sw_version", cJSON_CreateString(FANPICO_VERSION));
}
cJSON_AddItemToObject(json, "device", d);
if (!(buf = cJSON_Print(json)))
goto panic;
cJSON_Delete(json);
return buf;
panic:
cJSON_Delete(json);
return NULL;
}
static int send_ha_discovery_msg(const char *topic, const char *type, int idx, int count)
{
char *buf;
if (!(buf = json_ha_discovery_message(type, idx, count))) {
log_msg(LOG_WARNING, "json_ha_discovery_message(): failed");
return 1;
}
log_msg(LOG_INFO, "Publish HA Discovery Message: %s (%d)", topic, strlen(buf));
mqtt_publish_message(topic, buf, strlen(buf), mqtt_qos, 0, topic);
free(buf);
return 0;
}
static void fanpico_mqtt_ha_discovery()
{
char topic[128];
int count = 0;
if (mqtt_ha_discovery == 0)
return;
if (!time_passed(&t_mqtt_ha_discovery, mqtt_ha_discovery * 1000))
return;
mqtt_ha_discovery = 0;
for (int i = 0; i < SENSOR_COUNT; i++) {
if (cfg->mqtt_temp_mask & (1 << i)) {
snprintf(topic, sizeof(topic), "%s_t%d/config", mqtt_ha_base_topic, i + 1);
if (send_ha_discovery_msg(topic, "sensor", i + 1, count++))
return;
}
}
for (int i = 0; i < VSENSOR_COUNT; i++) {
if (cfg->mqtt_vtemp_mask & (1 << i)) {
snprintf(topic, sizeof(topic), "%s_vt%d/config", mqtt_ha_base_topic, i + 1);
if (send_ha_discovery_msg(topic, "vtemp", i + 1, count++))
return;
}
if (cfg->mqtt_vhumidity_mask & (1 << i)) {
snprintf(topic, sizeof(topic), "%s_vh%d/config", mqtt_ha_base_topic, i + 1);
if (send_ha_discovery_msg(topic, "vhumidity", i + 1, count++))
return;
}
if (cfg->mqtt_vpressure_mask & (1 << i)) {
snprintf(topic, sizeof(topic), "%s_vp%d/config", mqtt_ha_base_topic, i + 1);
if (send_ha_discovery_msg(topic, "vpressure", i + 1, count++))
return;
}
}
for (int i = 0; i < FAN_COUNT; i++) {
if (cfg->mqtt_fan_rpm_mask & (1 << i)) {
snprintf(topic, sizeof(topic), "%s_fr%d/config", mqtt_ha_base_topic, i + 1);
if (send_ha_discovery_msg(topic, "fanrpm", i + 1, count++))
return;
}
if (cfg->mqtt_fan_duty_mask & (1 << i)) {
snprintf(topic, sizeof(topic), "%s_fp%d/config", mqtt_ha_base_topic, i + 1);
if (send_ha_discovery_msg(topic, "fanpwm", i + 1, count++))
return;
}
}
for (int i = 0; i < MBFAN_COUNT; i++) {
if (cfg->mqtt_mbfan_rpm_mask & (1 << i)) {
snprintf(topic, sizeof(topic), "%s_mfr%d/config", mqtt_ha_base_topic, i + 1);
if (send_ha_discovery_msg(topic, "mbfanrpm", i + 1, count++))
return;
}
if (cfg->mqtt_mbfan_duty_mask & (1 << i)) {
snprintf(topic, sizeof(topic), "%s_mfp%d/config", mqtt_ha_base_topic, i + 1);
if (send_ha_discovery_msg(topic, "mbfanpwm", i + 1, count++))
return;
}
}
}
static char* json_ha_state_message()
{
const struct fanpico_state *st = fanpico_state;
cJSON *json;
char *buf, name[32];
if (!(json = cJSON_CreateObject()))
return NULL;
for (int i = 0; i < SENSOR_COUNT; i++) {
if (cfg->mqtt_temp_mask & (1 << i)) {
snprintf(name, sizeof(name), "sensor%d", i + 1);
cJSON_AddItemToObject(json, name, cJSON_CreateNumber(round_decimal(st->temp[i], 1)));
}
}
for (int i = 0; i < VSENSOR_COUNT; i++) {
if (cfg->mqtt_vtemp_mask & (1 << i)) {
snprintf(name, sizeof(name), "vtemp%d", i + 1);
cJSON_AddItemToObject(json, name, cJSON_CreateNumber(round_decimal(st->vtemp[i], 1)));
}
if (cfg->mqtt_vhumidity_mask & (1 << i)) {
snprintf(name, sizeof(name), "vhumidity%d", i + 1);
cJSON_AddItemToObject(json, name, cJSON_CreateNumber(round_decimal(st->vhumidity[i], 0)));
}
if (cfg->mqtt_vpressure_mask & (1 << i)) {
snprintf(name, sizeof(name), "vpressure%d", i + 1);
cJSON_AddItemToObject(json, name, cJSON_CreateNumber(round_decimal(st->vpressure[i], 0)));
}
}
for (int i = 0; i < FAN_COUNT; i++) {
if (cfg->mqtt_fan_rpm_mask & (1 << i)) {
float rpm = st->fan_freq[i] * 60 / cfg->fans[i].rpm_factor;
snprintf(name, sizeof(name), "fanrpm%d", i + 1);
cJSON_AddItemToObject(json, name, cJSON_CreateNumber(round_decimal(rpm, 0)));
}
if (cfg->mqtt_fan_duty_mask & (1 << i)) {
snprintf(name, sizeof(name), "fanpwm%d", i + 1);
cJSON_AddItemToObject(json, name, cJSON_CreateNumber(round_decimal(st->fan_duty[i], 1)));
}
}
for (int i = 0; i < MBFAN_COUNT; i++) {
if (cfg->mqtt_mbfan_rpm_mask & (1 << i)) {
float rpm = st->mbfan_freq[i] * 60 / cfg->mbfans[i].rpm_factor;
snprintf(name, sizeof(name), "mbfanrpm%d", i + 1);
cJSON_AddItemToObject(json, name, cJSON_CreateNumber(round_decimal(rpm, 0)));
}
if (cfg->mqtt_mbfan_duty_mask & (1 << i)) {
snprintf(name, sizeof(name), "mbfanpwm%d", i + 1);
cJSON_AddItemToObject(json, name, cJSON_CreateNumber(round_decimal(st->mbfan_duty[i], 1)));
}
}
if (!(buf = cJSON_Print(json)))
goto panic;
cJSON_Delete(json);
return buf;
panic:
cJSON_Delete(json);
return NULL;
}
static char* json_status_message()
{
const struct fanpico_state *st = fanpico_state;
char *buf;
cJSON *json, *l, *o;
int i;
float rpm;
time_t t;
if (!(json = cJSON_CreateObject()))
goto panic;
cJSON_AddItemToObject(json, "name", cJSON_CreateString(cfg->name));
cJSON_AddItemToObject(json, "hostname", cJSON_CreateString(net_state->hostname));
if (network_ip())
cJSON_AddItemToObject(json, "ip", cJSON_CreateString(network_ip()));
/* fans */
if (!(l = cJSON_CreateArray()))
goto panic;
cJSON_AddItemToObject(json, "fans", l);
for (i = 0; i < FAN_COUNT; i++) {
rpm = st->fan_freq[i] * 60 / cfg->fans[i].rpm_factor;
if (!(o = cJSON_CreateObject()))
goto panic;
cJSON_AddItemToObject(o, "id", cJSON_CreateNumber(i + 1));
cJSON_AddItemToObject(o, "rpm", cJSON_CreateNumber(rpm));
cJSON_AddItemToObject(o, "pwm", cJSON_CreateNumber(st->fan_duty[i]));
cJSON_AddItemToArray(l, o);
}
/* mbfans */
if (!(l = cJSON_CreateArray()))
goto panic;
cJSON_AddItemToObject(json, "mbfans", l);
for (i = 0; i < MBFAN_COUNT; i++) {
rpm = st->mbfan_freq[i] * 60 / cfg->mbfans[i].rpm_factor;
if (!(o = cJSON_CreateObject()))
goto panic;
cJSON_AddItemToObject(o, "id", cJSON_CreateNumber(i + 1));
cJSON_AddItemToObject(o, "rpm", cJSON_CreateNumber(rpm));
cJSON_AddItemToObject(o, "pwm", cJSON_CreateNumber(st->mbfan_duty[i]));
cJSON_AddItemToArray(l, o);
}
/* sensors */
if (!(l = cJSON_CreateArray()))
goto panic;
cJSON_AddItemToObject(json, "sensors", l);
for (i = 0; i < SENSOR_COUNT; i++) {
if (!(o = cJSON_CreateObject()))
goto panic;
cJSON_AddItemToObject(o, "id", cJSON_CreateNumber(i + 1));
cJSON_AddItemToObject(o, "temp", cJSON_CreateNumber(round_decimal(st->temp[i], 1)));
cJSON_AddItemToArray(l, o);
}
if ( rtc_get_time(&t) ) {
/* Send Data Time stamp to broker for possible use */
char datetime_buf[32];
time_t_to_str(datetime_buf, sizeof(datetime_buf), t);
cJSON_AddItemToObject(json, "datetime", cJSON_CreateString( datetime_buf ));
}
if (!(buf = cJSON_Print(json)))
goto panic;
cJSON_Delete(json);
return buf;
panic:
if (json)
cJSON_Delete(json);
return NULL;
}
void fanpico_mqtt_publish()
{
char *buf = NULL;
if (!mqtt_client)
return;
log_msg(LOG_DEBUG, "fanpico_mqtt_publish(): start");
if (strlen(cfg->mqtt_status_topic) > 0) {
/* Generate status message */
if (!(buf = json_status_message())) {
log_msg(LOG_WARNING,"json_status_message(): failed");
return;
}
mqtt_publish_message(cfg->mqtt_status_topic, buf, strlen(buf), mqtt_qos, 0,
cfg->mqtt_status_topic);
free(buf);
}
if (strlen(cfg->mqtt_ha_discovery_prefix) > 0) {
char topic[64 + 10 + 1];
if (mqtt_ha_discovery > 0)
fanpico_mqtt_ha_discovery();
/* Generate Home Assistant State message */
if (!(buf = json_ha_state_message())) {
log_msg(LOG_WARNING,"json_ha_state_message(): failed");
return;
}
snprintf(topic, sizeof(topic), "%s/state", mqtt_ha_base_topic);
mqtt_publish_message(topic, buf, strlen(buf), mqtt_qos, 0, topic);
free(buf);
}
log_msg(LOG_DEBUG, "fanpico_mqtt_publish(): end");
}
void fanpico_mqtt_publish_temp()
{
const struct fanpico_state *st = fanpico_state;
char topic[MQTT_MAX_TOPIC_LEN + 8];
char buf[64];
if (!mqtt_client || strlen(cfg->mqtt_temp_topic) < 1)
return;
log_msg(LOG_DEBUG, "fanpico_mqtt_publish_temp(): start");
for (int i = 0; i < SENSOR_COUNT; i++) {
if (cfg->mqtt_temp_mask & (1 << i)) {
snprintf(topic, sizeof(topic), cfg->mqtt_temp_topic, i + 1);
snprintf(buf, sizeof(buf), "%.1f", st->temp[i]);
mqtt_publish_message(topic, buf, strlen(buf), mqtt_qos, 0,
cfg->mqtt_temp_topic);
}
}
log_msg(LOG_DEBUG, "fanpico_mqtt_publish_temp(): end");
}
void fanpico_mqtt_publish_vsensor()
{
const struct fanpico_state *st = fanpico_state;
char topic[MQTT_MAX_TOPIC_LEN + 8];
char buf[64];
if (!mqtt_client)
return;
log_msg(LOG_DEBUG, "fanpico_mqtt_publish_vsensor(): start");
for (int i = 0; i < VSENSOR_COUNT; i++) {
if (strlen(cfg->mqtt_vtemp_topic) > 0 && cfg->mqtt_vtemp_mask & (1 << i)) {
snprintf(topic, sizeof(topic), cfg->mqtt_vtemp_topic, i + 1);
snprintf(buf, sizeof(buf), "%.1f", st->vtemp[i]);
mqtt_publish_message(topic, buf, strlen(buf), mqtt_qos, 0,
cfg->mqtt_vtemp_topic);
}
if (strlen(cfg->mqtt_vhumidity_topic) > 0 && cfg->mqtt_vhumidity_mask & (1 << i)) {
snprintf(topic, sizeof(topic), cfg->mqtt_vhumidity_topic, i + 1);
snprintf(buf, sizeof(buf), "%.0f", st->vhumidity[i]);
mqtt_publish_message(topic, buf, strlen(buf), mqtt_qos, 0,
cfg->mqtt_vhumidity_topic);
}
if (strlen(cfg->mqtt_vpressure_topic) > 0 && cfg->mqtt_vpressure_mask & (1 << i)) {
snprintf(topic, sizeof(topic), cfg->mqtt_vpressure_topic, i + 1);
snprintf(buf, sizeof(buf), "%.0f", st->vpressure[i]);
mqtt_publish_message(topic, buf, strlen(buf), mqtt_qos, 0,
cfg->mqtt_vpressure_topic);
}
}
log_msg(LOG_DEBUG, "fanpico_mqtt_publish_vsensor(): end");
}
void fanpico_mqtt_publish_rpm()
{
const struct fanpico_state *st = fanpico_state;
char topic[MQTT_MAX_TOPIC_LEN + 8];
char buf[64];
if (!mqtt_client)
return;
log_msg(LOG_DEBUG, "fanpico_mqtt_publish_rpm(): start");
if (strlen(cfg->mqtt_fan_rpm_topic) > 0) {
for (int i = 0; i < FAN_COUNT; i++) {
if (cfg->mqtt_fan_rpm_mask & (1 << i)) {
float rpm = st->fan_freq[i] * 60 / cfg->fans[i].rpm_factor;
snprintf(topic, sizeof(topic), cfg->mqtt_fan_rpm_topic, i + 1);
snprintf(buf, sizeof(buf), "%.0f", rpm);
mqtt_publish_message(topic, buf, strlen(buf), mqtt_qos, 0,
cfg->mqtt_fan_rpm_topic);
}
}
}
if (strlen(cfg->mqtt_mbfan_rpm_topic) > 0) {
for (int i = 0; i < MBFAN_COUNT; i++) {
if (cfg->mqtt_mbfan_rpm_mask & (1 << i)) {
float rpm = st->mbfan_freq[i] * 60 / cfg->mbfans[i].rpm_factor;
snprintf(topic, sizeof(topic), cfg->mqtt_mbfan_rpm_topic, i + 1);
snprintf(buf, sizeof(buf), "%.0f", rpm);
mqtt_publish_message(topic, buf, strlen(buf), mqtt_qos, 0,
cfg->mqtt_mbfan_rpm_topic);
}
}
}
log_msg(LOG_DEBUG, "fanpico_mqtt_publish_rpm(): end");
}
void fanpico_mqtt_publish_duty()
{
const struct fanpico_state *st = fanpico_state;
char topic[MQTT_MAX_TOPIC_LEN + 8];
char buf[64];
if (!mqtt_client)
return;
log_msg(LOG_DEBUG, "fanpico_mqtt_publish_duty(): start");
if (strlen(cfg->mqtt_fan_duty_topic) > 0) {
for (int i = 0; i < FAN_COUNT; i++) {
if (cfg->mqtt_fan_duty_mask & (1 << i)) {
snprintf(topic, sizeof(topic), cfg->mqtt_fan_duty_topic, i + 1);
snprintf(buf, sizeof(buf), "%.1f", st->fan_duty[i]);
mqtt_publish_message(topic, buf, strlen(buf), mqtt_qos, 0,
cfg->mqtt_fan_duty_topic);
}
}
}
if (strlen(cfg->mqtt_mbfan_duty_topic) > 0) {
for (int i = 0; i < MBFAN_COUNT; i++) {
if (cfg->mqtt_mbfan_duty_mask & (1 << i)) {
snprintf(topic, sizeof(topic), cfg->mqtt_mbfan_duty_topic, i + 1);
snprintf(buf, sizeof(buf), "%.1f", st->mbfan_duty[i]);
mqtt_publish_message(topic, buf, strlen(buf), mqtt_qos, 0,
cfg->mqtt_mbfan_duty_topic);
}
}
}
log_msg(LOG_DEBUG, "fanpico_mqtt_publish_duty(): end");
}
void fanpico_mqtt_scpi_command()
{
const struct fanpico_state *st = fanpico_state;
char cmd[MQTT_CMD_MAX_LEN];
int res;
if (!mqtt_client || !mqtt_scpi_cmd_queued)
return;
log_msg(LOG_DEBUG, "MQTT process SCPI command: '%s'", mqtt_scpi_cmd);
strncopy(cmd, mqtt_scpi_cmd, sizeof(cmd));
process_command(st, (struct fanpico_config *)cfg, cmd);
if ((res = last_command_status()) == 0) {
log_msg(LOG_INFO, "MQTT SCPI command successful: '%s'", mqtt_scpi_cmd);
send_mqtt_command_response(mqtt_scpi_cmd, res, "SCPI command successful");
} else {
log_msg(LOG_NOTICE, "MQTT SCPI command failed: '%s' (%d)", mqtt_scpi_cmd, res);
if (res == -113)
send_mqtt_command_response(mqtt_scpi_cmd, res, "SCPI unknown command");
else
send_mqtt_command_response(mqtt_scpi_cmd, res, "SCPI command failed");
}
mqtt_scpi_cmd[0] = 0;
mqtt_scpi_cmd_queued = false;
}
#endif /* WIFI_SUPPORT */
================================================
FILE: src/network.c
================================================
/* network.c
Copyright (C) 2022-2026 Timo Kokkonen
SPDX-License-Identifier: GPL-3.0-or-later
This file is part of FanPico.
FanPico is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FanPico is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FanPico. If not, see |