- warm_esp.{h,c}:warm_restart() 记下开着的路(RTC_NOINIT,魔数 + 掩码 + 反码)→ gpio_hold 锁五根 EN → 重启;
目标镜像没有能力标记(第 288 字节 PRCAPS1,.rodata_custom_desc)就不锁 —— 老固件写不动锁住的 EN。
warm_take():软件复位 + 标记对得上才接管,一次性;再读 EN 真电平核对
- bringup:adopt 掩码;rail_en_init_keep() 接管的路写高、五根一律放开 hold(崩溃时旧锁可能还在);
刀片不在 / mux 失联的接管路立刻关(SLOT_EMPTY arg 1);第二段已开着的路不重开、不等 PG
- pca9555_init_keep():方向寄存器是本固件写的才留 VBUS(掉过电的片子锁存器是 0xFF),键 / EDL 清零
- HAL:hal_gpio_hold();ESP 输出脚配 INPUT_OUTPUT(读得到真电平);sim 给 hold 建模
- 事件 LOG_EV_OTA arg 7 = 接管;兜底定时器回滚故意不走接管
- 顶层 CMake:phonerail.h 进 CMAKE_CONFIGURE_DEPENDS(否则镜像头版本停在旧值);-u g_phonerail_caps 防 gc
- test_bringup 加 6 例(接管 / 非计划复位全关 / 空槽 / mux 失联 / PCA 掉过电 / hold 冻结),全工程 448 例全绿
- rack:固件库识别能力标记,推送对话框按「支持 / 不支持重启不断电」给不同提示(已部署)
- 需求文档:SR-6 改为「非计划复位全关」+ 计划内重启例外;§10.9 新增「重启不断电」
- 板 B:台架镜像(空槽强制 CH1 在位,未提交)开 CH1 → OTA 带重启 → 事件 18/7 mv=1,CH1 一直开着;
现跑 0.1.0-c3。未测:示波器看重启期间 EN / 输出毛刺(要插刀片)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
387 行
15 KiB
C
387 行
15 KiB
C
/* C2:OTA 的平台无关部分(ota.h)+ `ota` 命令。
|
||
|
||
下载 / 写 flash / 切分区是 ESP 胶水,这里测不到;测的是会写错而上板不容易查出来的那几件:
|
||
· 参数校验:URL 只收 http、哈希只收 64 位小写十六进制、size 不许超过一个槽;
|
||
· 状态判定:下载中拒第二条、**待验证的固件不许再 OTA**(备用槽里是回滚用的旧版)、
|
||
同一个镜像已就绪时不重下;
|
||
· 判重:QoS 1 重投的同一条 ota 不会开工两次;
|
||
· 回滚判据:该通的那一层网络没通就不算健康,超时判回滚。 */
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#include "cmd.h"
|
||
#include "evlog.h"
|
||
#include "ota.h"
|
||
#include "sim_hal.h"
|
||
#include "unity.h"
|
||
|
||
static char s_out[CMD_OUT_MAX];
|
||
|
||
#define SHA_A "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
|
||
#define SHA_B "ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100"
|
||
#define URL_A "http://home.example:7765/fw/0011223344556677.bin"
|
||
|
||
/* 假胶水:记下收到的请求、可以让开工失败。 */
|
||
static int s_starts;
|
||
static int s_reboots;
|
||
static ota_req_t s_last;
|
||
static hal_err_t s_start_err;
|
||
|
||
static hal_err_t fake_start(void *ctx, const ota_req_t *req)
|
||
{
|
||
(void)ctx;
|
||
if (s_start_err != HAL_OK) {
|
||
return s_start_err;
|
||
}
|
||
s_last = *req;
|
||
s_starts++;
|
||
return HAL_OK;
|
||
}
|
||
|
||
static void fake_reboot(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
s_reboots++;
|
||
}
|
||
|
||
void setUp(void)
|
||
{
|
||
sim_hal_reset();
|
||
s_starts = 0;
|
||
s_reboots = 0;
|
||
s_start_err = HAL_OK;
|
||
memset(&s_last, 0, sizeof(s_last));
|
||
ota_init(fake_start, fake_reboot, NULL);
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||
cmd_init(NULL);
|
||
}
|
||
|
||
void tearDown(void) { TEST_ASSERT_EQUAL_INT(0, sim_crit_depth()); }
|
||
|
||
static const char *send_on(cmd_link_t link, const char *line)
|
||
{
|
||
cmd_handle_line(link, line, s_out, sizeof(s_out));
|
||
return s_out;
|
||
}
|
||
static const char *send(const char *line) { return send_on(CMD_LINK_WIFI, line); }
|
||
|
||
static void assert_has_at(const char *resp, const char *needle, int line)
|
||
{
|
||
UNITY_TEST_ASSERT_NOT_NULL(strstr(resp, needle), line, resp);
|
||
}
|
||
#define ASSERT_HAS(r, n) assert_has_at((r), (n), __LINE__)
|
||
|
||
static int count_rejects(void)
|
||
{
|
||
evlog_rec_t rec[64];
|
||
(void)evlog_flush(0);
|
||
const uint32_t n = evlog_read(0, rec, 64);
|
||
int k = 0;
|
||
for (uint32_t i = 0; i < n; i++) {
|
||
k += rec[i].type == LOG_EV_CMD_REJECT;
|
||
}
|
||
return k;
|
||
}
|
||
|
||
static void set_phase(ota_phase_t p, const char *sha_hex, bool pending)
|
||
{
|
||
ota_status_t st;
|
||
ota_status_get(&st);
|
||
st.phase = p;
|
||
st.pending = pending;
|
||
if (sha_hex != NULL) {
|
||
TEST_ASSERT_TRUE(ota_parse_sha256(sha_hex, st.sha256));
|
||
}
|
||
ota_status_set(&st);
|
||
}
|
||
|
||
/* ---- 校验 ---------------------------------------------------------------- */
|
||
|
||
static void test_url_rules(void)
|
||
{
|
||
TEST_ASSERT_TRUE(ota_url_ok(URL_A));
|
||
TEST_ASSERT_TRUE(ota_url_ok("http://10.5.5.13/a.bin"));
|
||
TEST_ASSERT_FALSE(ota_url_ok("https://home.example/a.bin")); /* 板子上没有下载服务器的 CA */
|
||
TEST_ASSERT_FALSE(ota_url_ok("ftp://x/a.bin"));
|
||
TEST_ASSERT_FALSE(ota_url_ok("http://"));
|
||
TEST_ASSERT_FALSE(ota_url_ok("http://a b/c.bin"));
|
||
TEST_ASSERT_FALSE(ota_url_ok("http://a/\"x"));
|
||
TEST_ASSERT_FALSE(ota_url_ok(NULL));
|
||
char long_url[OTA_URL_MAX + 2];
|
||
memset(long_url, 'a', sizeof(long_url));
|
||
memcpy(long_url, "http://", 7);
|
||
long_url[OTA_URL_MAX] = '\0';
|
||
TEST_ASSERT_TRUE(ota_url_ok(long_url)); /* 正好 64 */
|
||
long_url[OTA_URL_MAX] = 'a';
|
||
long_url[OTA_URL_MAX + 1] = '\0';
|
||
TEST_ASSERT_FALSE(ota_url_ok(long_url));
|
||
}
|
||
|
||
static void test_sha256_parse_is_strict_and_round_trips(void)
|
||
{
|
||
uint8_t sha[32];
|
||
TEST_ASSERT_TRUE(ota_parse_sha256(SHA_A, sha));
|
||
TEST_ASSERT_EQUAL_HEX8(0x00, sha[0]);
|
||
TEST_ASSERT_EQUAL_HEX8(0xff, sha[31]);
|
||
char hex[65];
|
||
ota_sha256_hex(sha, hex);
|
||
TEST_ASSERT_EQUAL_STRING(SHA_A, hex);
|
||
|
||
/* 大写不收:「同一个镜像」只许一种写法(READY 时靠它判是不是同一个) */
|
||
TEST_ASSERT_FALSE(ota_parse_sha256(
|
||
"00112233445566778899AABBCCDDEEFF00112233445566778899aabbccddeeff", sha));
|
||
TEST_ASSERT_FALSE(ota_parse_sha256(SHA_A "0", sha));
|
||
TEST_ASSERT_FALSE(ota_parse_sha256("0011", sha));
|
||
TEST_ASSERT_FALSE(ota_parse_sha256(
|
||
"g0112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", sha));
|
||
/* 半截错的不许写进 out */
|
||
uint8_t keep[32];
|
||
memset(keep, 0x5A, sizeof(keep));
|
||
TEST_ASSERT_FALSE(ota_parse_sha256(
|
||
"001122334455667788990000000000000000000000000000000000000000000z", keep));
|
||
TEST_ASSERT_EACH_EQUAL_HEX8(0x5A, keep, sizeof(keep));
|
||
}
|
||
|
||
/* ---- 判定 ---------------------------------------------------------------- */
|
||
|
||
static void test_plan(void)
|
||
{
|
||
ota_status_t st;
|
||
memset(&st, 0, sizeof(st));
|
||
ota_req_t r;
|
||
memset(&r, 0, sizeof(r));
|
||
TEST_ASSERT_TRUE(ota_parse_sha256(SHA_A, r.sha256));
|
||
|
||
TEST_ASSERT_EQUAL_INT(OTA_ACT_START, ota_plan(&st, &r));
|
||
st.phase = OTA_DL;
|
||
TEST_ASSERT_EQUAL_INT(OTA_ACT_BUSY, ota_plan(&st, &r));
|
||
|
||
st.phase = OTA_READY;
|
||
memcpy(st.sha256, r.sha256, 32);
|
||
TEST_ASSERT_EQUAL_INT(OTA_ACT_NOTHING, ota_plan(&st, &r));
|
||
r.reboot = true;
|
||
TEST_ASSERT_EQUAL_INT(OTA_ACT_REBOOT, ota_plan(&st, &r));
|
||
st.sha256[5] ^= 1; /* 已就绪的是另一个镜像 ⇒ 重下 */
|
||
TEST_ASSERT_EQUAL_INT(OTA_ACT_START, ota_plan(&st, &r));
|
||
|
||
/* ⭐ 待验证:备用槽里是回滚用的旧版,覆盖了就没有退路 */
|
||
st.phase = OTA_IDLE;
|
||
st.pending = true;
|
||
TEST_ASSERT_EQUAL_INT(OTA_ACT_PENDING, ota_plan(&st, &r));
|
||
}
|
||
|
||
/* ---- 命令 ---------------------------------------------------------------- */
|
||
|
||
/* ⭐ 正常开工:请求原样交给胶水,status 立刻看得到「在下载」,默认不重启。 */
|
||
static void test_ota_command_starts_a_download(void)
|
||
{
|
||
const char *r = send("{\"seq\":1,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A
|
||
"\",\"size\":987654}");
|
||
ASSERT_HAS(r, "\"ok\":true");
|
||
ASSERT_HAS(r, "\"act\":\"start\"");
|
||
TEST_ASSERT_EQUAL_INT(1, s_starts);
|
||
TEST_ASSERT_EQUAL_STRING(URL_A, s_last.url);
|
||
TEST_ASSERT_EQUAL_UINT32(987654, s_last.size);
|
||
TEST_ASSERT_FALSE(s_last.reboot); /* ⚠️ 默认不重启:重启 = 五路全断一次 */
|
||
char hex[65];
|
||
ota_sha256_hex(s_last.sha256, hex);
|
||
TEST_ASSERT_EQUAL_STRING(SHA_A, hex);
|
||
|
||
r = send("{\"cmd\":\"status\"}");
|
||
ASSERT_HAS(r, "\"ota\":{\"st\":\"dl\",\"got\":0,\"size\":987654,\"err\":null,\"pend\":false,"
|
||
"\"bad\":null}");
|
||
|
||
/* 下载中再来一条:busy,不开第二个 */
|
||
r = send("{\"seq\":2,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_B
|
||
"\",\"size\":1000,\"reboot\":true}");
|
||
ASSERT_HAS(r, "\"err\":\"busy\"");
|
||
TEST_ASSERT_EQUAL_INT(1, s_starts);
|
||
}
|
||
|
||
/* ⭐ QoS 1 重投:同一个 seq 的 ota 只开工一次,回第一次的结论。 */
|
||
static void test_redelivered_ota_starts_once(void)
|
||
{
|
||
const char *line = "{\"seq\":7,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A
|
||
"\",\"size\":1000,\"reboot\":true}";
|
||
ASSERT_HAS(send(line), "\"ok\":true");
|
||
ASSERT_HAS(send(line), "\"dup\":true");
|
||
TEST_ASSERT_EQUAL_INT(1, s_starts);
|
||
TEST_ASSERT_TRUE(s_last.reboot);
|
||
}
|
||
|
||
static void test_bad_arguments(void)
|
||
{
|
||
/* 缺字段 / 类型不对 / 格式不对:arg,不记事件 */
|
||
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"size\":1000}"),
|
||
"\"err\":\"arg\"");
|
||
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"ota\",\"url\":\"https://x/a\",\"sha256\":\"" SHA_A
|
||
"\",\"size\":1000}"),
|
||
"\"err\":\"arg\"");
|
||
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A
|
||
"\",\"size\":\"1000\"}"),
|
||
"\"err\":\"arg\"");
|
||
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A
|
||
"\",\"size\":1000,\"reboot\":1}"),
|
||
"\"err\":\"arg\""); /* reboot 是 true/false,不是 0/1 */
|
||
TEST_ASSERT_EQUAL_INT(0, count_rejects());
|
||
|
||
/* size 越界:range,记事件 */
|
||
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A
|
||
"\",\"size\":0}"),
|
||
"\"err\":\"range\"");
|
||
char line[256];
|
||
snprintf(line, sizeof(line),
|
||
"{\"seq\":6,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A "\",\"size\":%lu}",
|
||
(unsigned long)OTA_SLOT_BYTES + 1u);
|
||
ASSERT_HAS(send(line), "\"err\":\"range\"");
|
||
TEST_ASSERT_EQUAL_INT(2, count_rejects());
|
||
|
||
/* 没 seq:会动东西的命令必须带 */
|
||
ASSERT_HAS(send("{\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A "\",\"size\":10}"),
|
||
"\"err\":\"no_seq\"");
|
||
TEST_ASSERT_EQUAL_INT(0, s_starts);
|
||
}
|
||
|
||
/* ⭐ 待验证中的固件不许再 OTA:state + 记事件;胶水一次都不碰。 */
|
||
static void test_pending_image_refuses_ota(void)
|
||
{
|
||
set_phase(OTA_IDLE, NULL, true);
|
||
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A
|
||
"\",\"size\":1000}"),
|
||
"\"err\":\"state\"");
|
||
TEST_ASSERT_EQUAL_INT(0, s_starts);
|
||
TEST_ASSERT_EQUAL_INT(1, count_rejects());
|
||
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"pend\":true");
|
||
}
|
||
|
||
/* 同一个镜像已就绪:带 reboot 就直接重启、不重下;不带就什么都不做。 */
|
||
static void test_ready_image_is_not_downloaded_again(void)
|
||
{
|
||
set_phase(OTA_READY, SHA_A, false);
|
||
const char *r = send("{\"seq\":1,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A
|
||
"\",\"size\":1000}");
|
||
ASSERT_HAS(r, "\"act\":\"ready\"");
|
||
TEST_ASSERT_EQUAL_INT(0, s_reboots);
|
||
r = send("{\"seq\":2,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A
|
||
"\",\"size\":1000,\"reboot\":true}");
|
||
ASSERT_HAS(r, "\"act\":\"reboot\"");
|
||
TEST_ASSERT_EQUAL_INT(1, s_reboots);
|
||
TEST_ASSERT_EQUAL_INT(0, s_starts);
|
||
|
||
/* 换了个镜像:重下 */
|
||
r = send("{\"seq\":3,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_B
|
||
"\",\"size\":1000}");
|
||
ASSERT_HAS(r, "\"act\":\"start\"");
|
||
TEST_ASSERT_EQUAL_INT(1, s_starts);
|
||
}
|
||
|
||
/* 没有 OTA 能力(胶水没登记 / 任务起不来):state,状态回到 idle,不卡在 dl。 */
|
||
static void test_no_ota_capability(void)
|
||
{
|
||
s_start_err = HAL_ERR_STATE;
|
||
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A
|
||
"\",\"size\":1000}"),
|
||
"\"err\":\"state\"");
|
||
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"ota\":{\"st\":\"idle\"");
|
||
|
||
ota_init(NULL, NULL, NULL);
|
||
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"ota\",\"url\":\"" URL_A "\",\"sha256\":\"" SHA_A
|
||
"\",\"size\":1000}"),
|
||
"\"err\":\"state\"");
|
||
}
|
||
|
||
/* 经 USB 来的 ota 也收(下载走 WiFi,命令从哪来都行)。 */
|
||
static void test_ota_over_usb_is_accepted(void)
|
||
{
|
||
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"ota\",\"url\":\"" URL_A
|
||
"\",\"sha256\":\"" SHA_A "\",\"size\":1000}"),
|
||
"\"ok\":true");
|
||
TEST_ASSERT_EQUAL_INT(1, s_starts);
|
||
}
|
||
|
||
/* status 的 ota 一块:失败原因与回滚掉的版本号;版本号里的怪字符不许破坏 JSON。 */
|
||
static void test_status_reports_failure_and_rolled_back_version(void)
|
||
{
|
||
ota_status_t st;
|
||
ota_status_get(&st);
|
||
st.err = OTA_E_HASH;
|
||
st.size = 1234;
|
||
snprintf(st.bad, sizeof(st.bad), "0.1.0-\"x");
|
||
ota_status_set(&st);
|
||
const char *r = send("{\"cmd\":\"status\"}");
|
||
ASSERT_HAS(r, "\"ota\":{\"st\":\"idle\",\"got\":0,\"size\":1234,\"err\":\"hash\",\"pend\":false,"
|
||
"\"bad\":\"0.1.0-?x\"}");
|
||
}
|
||
|
||
/* ---- 健康判据 ------------------------------------------------------------ */
|
||
|
||
static void test_health(void)
|
||
{
|
||
ota_health_in_t in = { .pending = false };
|
||
TEST_ASSERT_EQUAL_INT(OTA_H_NONE, ota_health(&in));
|
||
|
||
in = (ota_health_in_t){ .pending = true, .now_ms = 5000, .boot_done = true, .wifi_set = true,
|
||
.wifi_up = true, .mqtt_set = true, .mqtt_up = false };
|
||
/* ⭐ 配了 broker:WiFi 通了不算数,MQTT 才是 OTA 的通道 */
|
||
TEST_ASSERT_EQUAL_INT(OTA_H_WAIT, ota_health(&in));
|
||
in.mqtt_up = true;
|
||
TEST_ASSERT_EQUAL_INT(OTA_H_VALID, ota_health(&in));
|
||
/* 启动没走完不算健康 */
|
||
in.boot_done = false;
|
||
TEST_ASSERT_EQUAL_INT(OTA_H_WAIT, ota_health(&in));
|
||
|
||
/* 超时:正好 120 s 判回滚,差 1 ms 不判 */
|
||
in = (ota_health_in_t){ .pending = true, .now_ms = OTA_HEALTH_MS - 1, .boot_done = true,
|
||
.wifi_set = true, .wifi_up = true, .mqtt_set = true };
|
||
TEST_ASSERT_EQUAL_INT(OTA_H_WAIT, ota_health(&in));
|
||
in.now_ms = OTA_HEALTH_MS;
|
||
TEST_ASSERT_EQUAL_INT(OTA_H_ROLLBACK, ota_health(&in));
|
||
/* 超时之后才连上:也算健康(胶水那一圈先问的是这个) */
|
||
in.mqtt_up = true;
|
||
TEST_ASSERT_EQUAL_INT(OTA_H_VALID, ota_health(&in));
|
||
|
||
/* 只配了 WiFi:WiFi 通就行 */
|
||
in = (ota_health_in_t){ .pending = true, .now_ms = 1000, .boot_done = true, .wifi_set = true };
|
||
TEST_ASSERT_EQUAL_INT(OTA_H_WAIT, ota_health(&in));
|
||
in.wifi_up = true;
|
||
TEST_ASSERT_EQUAL_INT(OTA_H_VALID, ota_health(&in));
|
||
|
||
/* 都没配(只经 USB 管的板子):启动走完就行,不会因为「没网」被误回滚 */
|
||
in = (ota_health_in_t){ .pending = true, .now_ms = 1000, .boot_done = true };
|
||
TEST_ASSERT_EQUAL_INT(OTA_H_VALID, ota_health(&in));
|
||
}
|
||
|
||
static void test_names(void)
|
||
{
|
||
TEST_ASSERT_EQUAL_STRING("ota", cmd_name(CMD_OTA));
|
||
TEST_ASSERT_EQUAL_STRING("hash", ota_err_str(OTA_E_HASH));
|
||
TEST_ASSERT_EQUAL_STRING("noslot", ota_err_str(OTA_E_NOSLOT));
|
||
TEST_ASSERT_EQUAL_STRING("?", ota_err_str(OTA_E_COUNT));
|
||
TEST_ASSERT_EQUAL_STRING("ready", ota_phase_str(OTA_READY));
|
||
/* 进 flash 的数字(LOG_EV_OTA 的 arg / bus_mv)钉死 */
|
||
TEST_ASSERT_EQUAL_INT(1, OTA_EV_START);
|
||
TEST_ASSERT_EQUAL_INT(6, OTA_EV_REBOOT);
|
||
TEST_ASSERT_EQUAL_INT(7, OTA_EV_WARM);
|
||
TEST_ASSERT_EQUAL_INT(3, OTA_E_HASH);
|
||
TEST_ASSERT_EQUAL_INT(7, OTA_E_NOSLOT);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
UNITY_BEGIN();
|
||
RUN_TEST(test_url_rules);
|
||
RUN_TEST(test_sha256_parse_is_strict_and_round_trips);
|
||
RUN_TEST(test_plan);
|
||
RUN_TEST(test_ota_command_starts_a_download);
|
||
RUN_TEST(test_redelivered_ota_starts_once);
|
||
RUN_TEST(test_bad_arguments);
|
||
RUN_TEST(test_pending_image_refuses_ota);
|
||
RUN_TEST(test_ready_image_is_not_downloaded_again);
|
||
RUN_TEST(test_no_ota_capability);
|
||
RUN_TEST(test_ota_over_usb_is_accepted);
|
||
RUN_TEST(test_status_reports_failure_and_rolled_back_version);
|
||
RUN_TEST(test_health);
|
||
RUN_TEST(test_names);
|
||
return UNITY_END();
|
||
}
|