diff --git a/CMakeLists.txt b/CMakeLists.txt index 26e4fe7..23b4194 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,8 @@ set(NL_FW_SOURCES src/net/nl_wifi_sm.c src/net/nl_wifi_w600.c src/net/nl_mdns.c + src/net/nl_http_server.c + src/sys/nl_app_api.c third_party/cJSON/cJSON.c) set(NL_FW ninlight_${NINLIGHT_DEVICE}) diff --git a/src/app_main.c b/src/app_main.c index 2c2337a..b92ef87 100644 --- a/src/app_main.c +++ b/src/app_main.c @@ -14,9 +14,11 @@ #include "light/nl_boot_state.h" #include "light/nl_control.h" #include "light/nl_engine.h" +#include "net/nl_http_server.h" #include "net/nl_mdns.h" #include "net/nl_wifi.h" #include "sys/nl_app.h" +#include "sys/nl_app_api.h" #include "sys/nl_console.h" #define NL_LIGHT_PRIO 30 @@ -186,6 +188,8 @@ static void app_task(void *arg) nl_wifi_start(NINLIGHT_WIFI_SSID, NINLIGHT_WIFI_PASSWORD, NINLIGHT_AP_PASSWORD); } + nl_http_server_start(nl_app_api_ctx()); + boot_ms = nl_app_now_ms(); beat_seen = s_light_beat; for (;;) { diff --git a/src/net/nl_http_server.c b/src/net/nl_http_server.c new file mode 100644 index 0000000..084fcf0 --- /dev/null +++ b/src/net/nl_http_server.c @@ -0,0 +1,104 @@ +#include + +#include "wm_include.h" +#include "wm_sockets.h" + +#include "net/nl_http.h" +#include "net/nl_http_server.h" +#include "sys/nl_app.h" + +#define NL_HTTP_PRIO 42 + +static u32 s_stack[1536]; /* 6 KB: cJSON + API handler */ +static char s_buf[NL_HTTP_MAX_REQUEST]; +static char s_out[NL_API_BODY_MAX + 160]; +static nl_api_response_t s_resp; +static const nl_api_ctx_t *s_ctx; + +static void send_all(int fd, const char *p, size_t n) +{ + while (n) { + int w = send(fd, p, n, 0); + if (w <= 0) + return; + p += w; + n -= (size_t)w; + } +} + +static void simple_error(int status, const char *msg) +{ + s_resp.status = status; + s_resp.after = NL_API_AFTER_NONE; + strcpy(s_resp.body, msg); +} + +static void serve(int fd) +{ + struct timeval tv; + nl_http_request_t req; + nl_http_parse_t pr = NL_HTTP_INCOMPLETE; + size_t len = 0, out; + + tv.tv_sec = 5; + tv.tv_usec = 0; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + while (pr == NL_HTTP_INCOMPLETE && len < sizeof s_buf) { + int n = recv(fd, s_buf + len, sizeof s_buf - len, 0); + if (n <= 0) + return; /* timeout or closed: drop silently */ + len += (size_t)n; + pr = nl_http_parse(s_buf, len, &req); + } + if (pr == NL_HTTP_COMPLETE) + nl_api_handle(s_ctx, &req, &s_resp); + else if (pr == NL_HTTP_BAD_REQUEST) + simple_error(400, "{\"error\":\"bad request\"}"); + else + simple_error(413, "{\"error\":\"request too large\"}"); + out = nl_http_response(s_out, sizeof s_out, s_resp.status, s_resp.body); + if (out) + send_all(fd, s_out, out); +} + +static void server_task(void *arg) +{ + (void)arg; + for (;;) { + struct sockaddr_in addr; + int ls = socket(AF_INET, SOCK_STREAM, 0); + + memset(&addr, 0, sizeof addr); + addr.sin_family = AF_INET; + addr.sin_port = htons(NL_HTTP_PORT); + addr.sin_addr.s_addr = htonl(INADDR_ANY); + if (ls < 0 || bind(ls, (struct sockaddr *)&addr, sizeof addr) != 0 || listen(ls, 4) != 0) { + if (ls >= 0) + closesocket(ls); + tls_os_time_delay(HZ); + continue; + } + for (;;) { + struct sockaddr_in peer; + socklen_t plen = sizeof peer; + int fd = accept(ls, (struct sockaddr *)&peer, &plen); + if (fd < 0) { + tls_os_time_delay(HZ / 10); + continue; + } + s_resp.after = NL_API_AFTER_NONE; + serve(fd); + closesocket(fd); + if (s_resp.after == NL_API_AFTER_REBOOT) { + tls_os_time_delay(HZ / 2); /* let the TCP stack flush the response */ + nl_app_reboot(); + } + } + } +} + +void nl_http_server_start(const nl_api_ctx_t *ctx) +{ + s_ctx = ctx; + tls_os_task_create(NULL, "nl_http", server_task, NULL, (u8 *)s_stack, sizeof(s_stack), NL_HTTP_PRIO, 0); +} diff --git a/src/net/nl_http_server.h b/src/net/nl_http_server.h new file mode 100644 index 0000000..326e3dc --- /dev/null +++ b/src/net/nl_http_server.h @@ -0,0 +1,11 @@ +#ifndef NL_HTTP_SERVER_H +#define NL_HTTP_SERVER_H + +#include "net/nl_api.h" + +#define NL_HTTP_PORT 3333 + +/* Serves one connection at a time (listen backlog 4, 5 s receive timeout). */ +void nl_http_server_start(const nl_api_ctx_t *ctx); + +#endif diff --git a/src/sys/nl_app_api.c b/src/sys/nl_app_api.c new file mode 100644 index 0000000..08cc389 --- /dev/null +++ b/src/sys/nl_app_api.c @@ -0,0 +1,131 @@ +#include + +#include "wm_include.h" +#include "wm_mem.h" + +#include "cJSON.h" +#include "light/nl_control.h" +#include "light/nl_engine.h" +#include "net/nl_mdns.h" +#include "net/nl_names.h" +#include "net/nl_wifi.h" +#include "sys/nl_app.h" +#include "sys/nl_app_api.h" + +static void *json_alloc(size_t n) { return tls_mem_alloc(n); } +static void json_free(void *p) { tls_mem_free(p); } + +static void get_state(nl_light_state_t *out) +{ + nl_app_lock(); + nl_ctrl_get(out); + nl_app_unlock(); +} + +static nl_ctrl_err_t apply(const nl_light_change_t *c) +{ + nl_ctrl_err_t e; + nl_app_lock(); + e = nl_ctrl_apply(c, nl_app_now_ms()); + nl_app_unlock(); + return e; +} + +static void get_settings(nl_settings_t *out) +{ + nl_app_lock(); + *out = *nl_app_settings(); + nl_app_unlock(); +} + +/* The API never changes Wi-Fi credentials through here (that goes through the onboarding test), + * so the current ones are kept even if the network task stored new ones meanwhile. */ +static int save_settings(const nl_settings_t *s) +{ + nl_settings_t *cur; + char name[NL_NAME_LEN]; + bool name_changed; + + nl_app_lock(); + cur = nl_app_settings(); + name_changed = strcmp(cur->name, s->name) != 0; + { + nl_settings_t merged = *s; + memcpy(merged.wifi_ssid, cur->wifi_ssid, sizeof merged.wifi_ssid); + memcpy(merged.wifi_pass, cur->wifi_pass, sizeof merged.wifi_pass); + *cur = merged; + } + nl_ctrl_set_fades(cur->fade_in_ms, cur->fade_out_ms); + memcpy(name, cur->name, sizeof name); + nl_app_unlock(); + if (name_changed) + nl_mdns_set_name(name); + return nl_app_save_settings(); +} + +static int wifi_configure(const char *ssid, const char *pass) { return nl_wifi_request_onboard(ssid, pass); } +static bool open_access(void) { return nl_wifi_open_access(); } + +static void get_info(nl_sys_info_t *out) +{ + nl_wifi_status_t w; + uint8_t mac[6]; + + memset(out, 0, sizeof *out); + nl_wifi_mac(mac); + nl_names_mac_str(mac, out->mac); + nl_app_lock(); + memcpy(out->name, nl_app_settings()->name, sizeof out->name); + nl_app_unlock(); + nl_wifi_status(&w); + out->wifi_mode = w.mode; + memcpy(out->ssid, w.ssid, sizeof out->ssid); + out->rssi = w.rssi; + memcpy(out->ip, w.ip, sizeof out->ip); + memcpy(out->sta_ip, w.sta_ip, sizeof out->sta_ip); + out->wifi_error = w.error; + out->uptime_s = nl_app_now_ms() / 1000u; + out->free_heap = tls_mem_get_avail_heapsize(); + out->last_ota_error = NULL; +} + +static void factory_reset(void) { nl_app_request_factory_reset(); } + +static void ping(void) +{ + nl_app_lock(); + nl_engine_blink(3); + nl_app_unlock(); +} + +static int fw_update(const char *url) +{ + (void)url; + return -1; /* OTA arrives in plan 3 */ +} + +const nl_api_ctx_t *nl_app_api_ctx(void) +{ + static nl_api_ctx_t ctx; + static bool ready; + + if (!ready) { + cJSON_Hooks hooks = {json_alloc, json_free}; + cJSON_InitHooks(&hooks); + ctx.board = nl_board_get(); + ctx.fw_version = NINLIGHT_FW_VERSION; + ctx.build_date = NINLIGHT_BUILD_DATE; + ctx.get_state = get_state; + ctx.apply = apply; + ctx.get_settings = get_settings; + ctx.save_settings = save_settings; + ctx.wifi_configure = wifi_configure; + ctx.open_access = open_access; + ctx.get_info = get_info; + ctx.factory_reset = factory_reset; + ctx.ping = ping; + ctx.fw_update = fw_update; + ready = true; + } + return &ctx; +} diff --git a/src/sys/nl_app_api.h b/src/sys/nl_app_api.h new file mode 100644 index 0000000..b205b16 --- /dev/null +++ b/src/sys/nl_app_api.h @@ -0,0 +1,9 @@ +#ifndef NL_APP_API_H +#define NL_APP_API_H + +#include "net/nl_api.h" + +/* API context backed by the running application. First call installs the cJSON allocator. */ +const nl_api_ctx_t *nl_app_api_ctx(void); + +#endif