Skip to content

Commit c4c1520

Browse files
authored
Merge pull request #6608 from tannewt/web_workflow_port
Allow for dynamic reconfigure including port
2 parents 282f8c4 + b11eb12 commit c4c1520

File tree

6 files changed

+129
-78
lines changed

6 files changed

+129
-78
lines changed

docs/environment.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ CIRCUITPY_WEB_API_PASSWORD
3939
~~~~~~~~~~~~~~~~~~~~~~~~~~
4040
Password required to make modifications to the board from the Web Workflow.
4141

42+
CIRCUITPY_WEB_API_PORT
43+
~~~~~~~~~~~~~~~~~~~~~~
44+
TCP port number used for the web HTTP API. Defaults to 80 when omitted.
45+
4246
CIRCUITPY_WIFI_PASSWORD
4347
~~~~~~~~~~~~~~~~~~~~~~~
4448
Wi-Fi password used to auto connect to CIRCUITPY_WIFI_SSID.

docs/workflows.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Read-only characteristic that returns the UTF-8 encoded version string.
7272
The web workflow is depends on adding Wi-Fi credentials into the `/.env` file. The keys are
7373
`CIRCUITPY_WIFI_SSID` and `CIRCUITPY_WIFI_PASSWORD`. Once these are defined, CircuitPython will
7474
automatically connect to the network and start the webserver used for the workflow. The webserver
75-
is on port 80. It also enables MDNS.
75+
is on port 80 unless overridden by `CIRCUITPY_WEB_API_PORT`. It also enables MDNS.
7676

7777
Here is an example `/.env`:
7878

@@ -83,6 +83,8 @@ CIRCUITPY_WIFI_PASSWORD='secretpassword'
8383

8484
# To enable modifying files from the web. Change this too!
8585
CIRCUITPY_WEB_API_PASSWORD='passw0rd'
86+
87+
CIRCUITPY_WEB_API_PORT=80
8688
```
8789

8890
MDNS is used to resolve [`circuitpython.local`](http://circuitpython.local) to a device specific

ports/espressif/common-hal/mdns/Server.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,5 +204,9 @@ mp_obj_t common_hal_mdns_server_find(mdns_server_obj_t *self, const char *servic
204204
}
205205

206206
void common_hal_mdns_server_advertise_service(mdns_server_obj_t *self, const char *service_type, const char *protocol, mp_int_t port) {
207-
mdns_service_add(NULL, service_type, protocol, port, NULL, 0);
207+
if (mdns_service_exists(service_type, protocol, NULL)) {
208+
mdns_service_port_set(service_type, protocol, port);
209+
} else {
210+
mdns_service_add(NULL, service_type, protocol, port, NULL, 0);
211+
}
208212
}

shared-bindings/mdns/Server.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mdns_server_find_obj, 1, _mdns_server_find);
161161
//| def advertise_service(self, *, service_type: str, protocol: str, port: int) -> None:
162162
//| """Respond to queries for the given service with the given port.
163163
//|
164+
//| ``service_type`` and ``protocol`` can only occur on one port. Any call after the first
165+
//| will update the entry's port.
166+
//|
164167
//| :param str service_type: The service type such as "_http"
165168
//| :param str protocol: The service protocol such as "_tcp"
166169
//| :param int port: The port used by the service"""

supervisor/shared/web_workflow/static/welcome.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async function find_devices() {
4444
li.appendChild(a);
4545
var port = "";
4646
if (device.port != 80) {
47-
port = ":" + version_info.port;
47+
port = ":" + device.port;
4848
}
4949
var server;
5050
if (mdns_works) {

supervisor/shared/web_workflow/web_workflow.c

Lines changed: 113 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ typedef struct {
9898
static wifi_radio_error_t wifi_status = WIFI_RADIO_ERROR_NONE;
9999

100100
static mdns_server_obj_t mdns;
101+
static uint32_t web_api_port = 80;
101102

102103
static socketpool_socketpool_obj_t pool;
103104
static socketpool_socket_obj_t listening;
@@ -189,6 +190,9 @@ void supervisor_web_workflow_status(void) {
189190
}
190191

191192
mp_printf(&mp_plat_print, "%s", _our_ip_encoded);
193+
if (web_api_port != 80) {
194+
mp_printf(&mp_plat_print, ":%d", web_api_port);
195+
}
192196
// TODO: Use these unicode to show signal strength: ▂▄▆█
193197
}
194198
} else {
@@ -199,11 +203,6 @@ void supervisor_web_workflow_status(void) {
199203
void supervisor_start_web_workflow(void) {
200204
#if CIRCUITPY_WEB_WORKFLOW && CIRCUITPY_WIFI
201205

202-
if (common_hal_wifi_radio_get_enabled(&common_hal_wifi_radio_obj) &&
203-
wifi_radio_get_ipv4_address(&common_hal_wifi_radio_obj) != 0) {
204-
// Already started.
205-
return;
206-
}
207206

208207
char ssid[33];
209208
char password[64];
@@ -218,15 +217,20 @@ void supervisor_start_web_workflow(void) {
218217
password_len <= 0 || (size_t)password_len >= sizeof(password)) {
219218
return;
220219
}
221-
common_hal_wifi_init(false);
222-
common_hal_wifi_radio_set_enabled(&common_hal_wifi_radio_obj, true);
220+
if (!common_hal_wifi_radio_get_enabled(&common_hal_wifi_radio_obj)) {
221+
common_hal_wifi_init(false);
222+
common_hal_wifi_radio_set_enabled(&common_hal_wifi_radio_obj, true);
223+
}
223224

224225
// TODO: Do our own scan so that we can find the channel we want before calling connect.
225226
// Otherwise, connect will do a full slow scan to pick the best AP.
226227

227228
// NUL terminate the strings because dotenv doesn't.
228229
ssid[ssid_len] = '\0';
229230
password[password_len] = '\0';
231+
// We can all connect again because it will return early if we're already connected to the
232+
// network. If we are connected to a different network, then it will disconnect before
233+
// attempting to connect to the given network.
230234
wifi_status = common_hal_wifi_radio_connect(
231235
&common_hal_wifi_radio_obj, (uint8_t *)ssid, ssid_len, (uint8_t *)password, password_len,
232236
0, 0.1, NULL, 0);
@@ -236,21 +240,47 @@ void supervisor_start_web_workflow(void) {
236240
return;
237241
}
238242

239-
mdns_server_construct(&mdns, true);
240-
common_hal_mdns_server_set_instance_name(&mdns, MICROPY_HW_BOARD_NAME);
241-
common_hal_mdns_server_advertise_service(&mdns, "_circuitpython", "_tcp", 80);
243+
char port_encoded[6];
244+
size_t port_len = 0;
245+
size_t new_port = web_api_port;
246+
#if CIRCUITPY_DOTENV
247+
port_len = dotenv_get_key("/.env", "CIRCUITPY_WEB_API_PORT", port_encoded, sizeof(port_encoded) - 1);
248+
#endif
249+
if (0 < port_len && port_len < sizeof(port_encoded)) {
250+
port_encoded[port_len] = '\0';
251+
new_port = strtoul(port_encoded, NULL, 10);
252+
}
253+
254+
bool first_start = mdns.base.type != &mdns_server_type;
255+
bool port_changed = new_port != web_api_port;
242256

243-
pool.base.type = &socketpool_socketpool_type;
244-
common_hal_socketpool_socketpool_construct(&pool, &common_hal_wifi_radio_obj);
257+
if (first_start) {
258+
ESP_LOGI(TAG, "Starting web workflow");
259+
mdns_server_construct(&mdns, true);
260+
mdns.base.type = &mdns_server_type;
261+
common_hal_mdns_server_set_instance_name(&mdns, MICROPY_HW_BOARD_NAME);
262+
pool.base.type = &socketpool_socketpool_type;
263+
common_hal_socketpool_socketpool_construct(&pool, &common_hal_wifi_radio_obj);
245264

246-
ESP_LOGI(TAG, "Starting web workflow");
247-
listening.base.type = &socketpool_socket_type;
248-
socketpool_socket(&pool, SOCKETPOOL_AF_INET, SOCKETPOOL_SOCK_STREAM, &listening);
249-
common_hal_socketpool_socket_settimeout(&listening, 0);
250-
// Bind to any ip.
251-
// TODO: Make this port .env configurable.
252-
common_hal_socketpool_socket_bind(&listening, "", 0, 80);
253-
common_hal_socketpool_socket_listen(&listening, 1);
265+
listening.base.type = &socketpool_socket_type;
266+
active.base.type = &socketpool_socket_type;
267+
active.num = -1;
268+
active.connected = false;
269+
270+
websocket_init();
271+
}
272+
if (port_changed) {
273+
common_hal_socketpool_socket_close(&listening);
274+
}
275+
if (first_start || port_changed) {
276+
web_api_port = new_port;
277+
common_hal_mdns_server_advertise_service(&mdns, "_circuitpython", "_tcp", web_api_port);
278+
socketpool_socket(&pool, SOCKETPOOL_AF_INET, SOCKETPOOL_SOCK_STREAM, &listening);
279+
common_hal_socketpool_socket_settimeout(&listening, 0);
280+
// Bind to any ip.
281+
common_hal_socketpool_socket_bind(&listening, "", 0, web_api_port);
282+
common_hal_socketpool_socket_listen(&listening, 1);
283+
}
254284

255285
mp_int_t api_password_len = dotenv_get_key("/.env", "CIRCUITPY_WEB_API_PASSWORD", _api_password + 1, sizeof(_api_password) - 2);
256286
if (api_password_len > 0) {
@@ -259,12 +289,6 @@ void supervisor_start_web_workflow(void) {
259289
_base64_in_place(_api_password, api_password_len + 1, sizeof(_api_password));
260290
}
261291

262-
active.base.type = &socketpool_socket_type;
263-
active.num = -1;
264-
active.connected = false;
265-
266-
websocket_init();
267-
268292
// TODO:
269293
// GET /cp/serial.txt
270294
// - Most recent 1k of serial output.
@@ -283,6 +307,10 @@ static void _send_raw(socketpool_socket_obj_t *socket, const uint8_t *buf, int l
283307
}
284308
}
285309

310+
STATIC void _print_raw(void *env, const char *str, size_t len) {
311+
_send_raw((socketpool_socket_obj_t *)env, (const uint8_t *)str, (size_t)len);
312+
}
313+
286314
static void _send_str(socketpool_socket_obj_t *socket, const char *str) {
287315
_send_raw(socket, (const uint8_t *)str, strlen(str));
288316
}
@@ -301,14 +329,19 @@ static void _send_strs(socketpool_socket_obj_t *socket, ...) {
301329
}
302330

303331
static void _send_chunk(socketpool_socket_obj_t *socket, const char *chunk) {
304-
char encoded_len[sizeof(size_t) * 2 + 1];
305-
int len = snprintf(encoded_len, sizeof(encoded_len), "%X", strlen(chunk));
306-
_send_raw(socket, (const uint8_t *)encoded_len, len);
307-
_send_raw(socket, (const uint8_t *)"\r\n", 2);
332+
mp_print_t _socket_print = {socket, _print_raw};
333+
mp_printf(&_socket_print, "%X\r\n", strlen(chunk));
308334
_send_raw(socket, (const uint8_t *)chunk, strlen(chunk));
309335
_send_raw(socket, (const uint8_t *)"\r\n", 2);
310336
}
311337

338+
STATIC void _print_chunk(void *env, const char *str, size_t len) {
339+
mp_print_t _socket_print = {env, _print_raw};
340+
mp_printf(&_socket_print, "%X\r\n", len);
341+
_send_raw((socketpool_socket_obj_t *)env, (const uint8_t *)str, len);
342+
_send_raw((socketpool_socket_obj_t *)env, (const uint8_t *)"\r\n", 2);
343+
}
344+
312345
// A bit of a misnomer because it sends all arguments as one chunk.
313346
// The last argument must be NULL! Otherwise, it won't stop.
314347
static void _send_chunks(socketpool_socket_obj_t *socket, ...) {
@@ -326,9 +359,9 @@ static void _send_chunks(socketpool_socket_obj_t *socket, ...) {
326359
}
327360
va_end(strs_to_count);
328361

329-
char encoded_len[sizeof(size_t) * 2 + 1];
330-
snprintf(encoded_len, sizeof(encoded_len), "%X", chunk_len);
331-
_send_strs(socket, encoded_len, "\r\n", NULL);
362+
363+
mp_print_t _socket_print = {socket, _print_raw};
364+
mp_printf(&_socket_print, "%X\r\n", chunk_len);
332365

333366
str = va_arg(strs_to_send, const char *);
334367
while (str != NULL) {
@@ -531,7 +564,12 @@ static void _reply_redirect(socketpool_socket_obj_t *socket, _request *request,
531564
_send_str(socket, "http");
532565
}
533566

534-
_send_strs(socket, "://", hostname, ".local", path, "\r\n", NULL);
567+
_send_strs(socket, "://", hostname, ".local", NULL);
568+
if (web_api_port != 80) {
569+
mp_print_t _socket_print = {socket, _print_raw};
570+
mp_printf(&_socket_print, ":%d", web_api_port);
571+
}
572+
_send_strs(socket, path, "\r\n", NULL);
535573
_cors_header(socket, request);
536574
_send_str(socket, "\r\n");
537575
}
@@ -540,6 +578,7 @@ static void _reply_directory_json(socketpool_socket_obj_t *socket, _request *req
540578
socketpool_socket_send(socket, (const uint8_t *)OK_JSON, strlen(OK_JSON));
541579
_cors_header(socket, request);
542580
_send_str(socket, "\r\n");
581+
mp_print_t _socket_print = {socket, _print_chunk};
543582
_send_chunk(socket, "[");
544583
bool first = true;
545584

@@ -560,7 +599,7 @@ static void _reply_directory_json(socketpool_socket_obj_t *socket, _request *req
560599
}
561600
// We use nanoseconds past Jan 1, 1970 for consistency with BLE API and
562601
// LittleFS.
563-
_send_chunk(socket, ", \"modified_ns\": ");
602+
_send_chunk(socket, ", ");
564603

565604
uint64_t truncated_time = timeutils_mktime(1980 + (file_info.fdate >> 9),
566605
(file_info.fdate >> 5) & 0xf,
@@ -569,15 +608,17 @@ static void _reply_directory_json(socketpool_socket_obj_t *socket, _request *req
569608
(file_info.ftime >> 5) & 0x1f,
570609
(file_info.ftime & 0x1f) * 2) * 1000000000ULL;
571610

572-
char encoded_number[32];
573-
snprintf(encoded_number, sizeof(encoded_number), "%lld", truncated_time);
574-
_send_chunks(socket, encoded_number, ", \"file_size\": ", NULL);
611+
// Use snprintf because mp_printf doesn't support 64 bit numbers by
612+
// default.
613+
char encoded_time[32];
614+
snprintf(encoded_time, sizeof(encoded_time), "%llu", truncated_time);
615+
mp_printf(&_socket_print, "\"modified_ns\": %s, ", encoded_time);
575616
size_t file_size = 0;
576617
if ((file_info.fattrib & AM_DIR) == 0) {
577618
file_size = file_info.fsize;
578619
}
579-
snprintf(encoded_number, sizeof(encoded_number), "%d", file_size);
580-
_send_chunks(socket, encoded_number, "}", NULL);
620+
mp_printf(&_socket_print, "\"file_size\": %d }", file_size);
621+
581622
first = false;
582623
res = f_readdir(dir, &file_info);
583624
}
@@ -587,12 +628,10 @@ static void _reply_directory_json(socketpool_socket_obj_t *socket, _request *req
587628

588629
static void _reply_with_file(socketpool_socket_obj_t *socket, _request *request, const char *filename, FIL *active_file) {
589630
uint32_t total_length = f_size(active_file);
590-
char encoded_len[10];
591-
snprintf(encoded_len, sizeof(encoded_len), "%d", total_length);
592631

593-
_send_strs(socket,
594-
"HTTP/1.1 200 OK\r\n",
595-
"Content-Length: ", encoded_len, "\r\n", NULL);
632+
_send_str(socket, "HTTP/1.1 200 OK\r\n");
633+
mp_print_t _socket_print = {socket, _print_raw};
634+
mp_printf(&_socket_print, "Content-Length: %d\r\n", total_length);
596635
// TODO: Make this a table to save space.
597636
if (_endswith(filename, ".txt") || _endswith(filename, ".py")) {
598637
_send_str(socket, "Content-Type: text/plain\r\n");
@@ -640,27 +679,23 @@ static void _reply_with_devices_json(socketpool_socket_obj_t *socket, _request *
640679
socketpool_socket_send(socket, (const uint8_t *)OK_JSON, strlen(OK_JSON));
641680
_cors_header(socket, request);
642681
_send_str(socket, "\r\n");
643-
char total_encoded[4];
644-
snprintf(total_encoded, sizeof(total_encoded), "%d", total_results);
645-
_send_chunks(socket, "{\"total\": ", total_encoded, ", \"devices\": [", NULL);
682+
mp_print_t _socket_print = {socket, _print_chunk};
683+
684+
mp_printf(&_socket_print, "{\"total\": %d, \"devices\": [", total_results);
646685
for (size_t i = 0; i < count; i++) {
647686
if (i > 0) {
648687
_send_chunk(socket, ",");
649688
}
650689
const char *hostname = common_hal_mdns_remoteservice_get_hostname(&found_devices[i]);
651690
const char *instance_name = common_hal_mdns_remoteservice_get_instance_name(&found_devices[i]);
652-
char port_encoded[4];
653691
int port = common_hal_mdns_remoteservice_get_port(&found_devices[i]);
654-
snprintf(port_encoded, sizeof(port_encoded), "%d", port);
655-
char ip_encoded[4 * 4];
656692
uint32_t ipv4_address = mdns_remoteservice_get_ipv4_address(&found_devices[i]);
657693
uint8_t *octets = (uint8_t *)&ipv4_address;
658-
snprintf(ip_encoded, sizeof(ip_encoded), "%d.%d.%d.%d", octets[0], octets[1], octets[2], octets[3]);
659-
_send_chunks(socket,
660-
"{\"hostname\": \"", hostname, "\", ",
661-
"\"instance_name\": \"", instance_name, "\", ",
662-
"\"port\": ", port_encoded, ", ",
663-
"\"ip\": \"", ip_encoded, "\"}", NULL);
694+
mp_printf(&_socket_print,
695+
"{\"hostname\": \"%s\", "
696+
"\"instance_name\": \"%s\", "
697+
"\"port\": %d, "
698+
"\"ip\": \"%d.%d.%d.%d\"}", hostname, instance_name, port, octets[0], octets[1], octets[2], octets[3]);
664699
common_hal_mdns_remoteservice_deinit(&found_devices[i]);
665700
}
666701
_send_chunk(socket, "]}");
@@ -672,24 +707,22 @@ static void _reply_with_version_json(socketpool_socket_obj_t *socket, _request *
672707
_send_str(socket, OK_JSON);
673708
_cors_header(socket, request);
674709
_send_str(socket, "\r\n");
675-
char encoded_creator_id[11]; // 2 ** 32 is 10 decimal digits plus one for \0
676-
snprintf(encoded_creator_id, sizeof(encoded_creator_id), "%u", CIRCUITPY_CREATOR_ID);
677-
char encoded_creation_id[11]; // 2 ** 32 is 10 decimal digits plus one for \0
678-
snprintf(encoded_creation_id, sizeof(encoded_creation_id), "%u", CIRCUITPY_CREATION_ID);
710+
mp_print_t _socket_print = {socket, _print_chunk};
711+
679712
const char *hostname = common_hal_mdns_server_get_hostname(&mdns);
680-
_send_chunks(socket,
681-
"{\"web_api_version\": 1, ",
682-
"\"version\": \"", MICROPY_GIT_TAG, "\", ",
683-
"\"build_date\": \"", MICROPY_BUILD_DATE, "\", ",
684-
"\"board_name\": \"", MICROPY_HW_BOARD_NAME, "\", ",
685-
"\"mcu_name\": \"", MICROPY_HW_MCU_NAME, "\", ",
686-
"\"board_id\": \"", CIRCUITPY_BOARD_ID, "\", ",
687-
"\"creator_id\": ", encoded_creator_id, ", ",
688-
"\"creation_id\": ", encoded_creation_id, ", ",
689-
"\"hostname\": \"", hostname, "\", ",
690-
"\"port\": 80, ",
691-
"\"ip\": \"", _our_ip_encoded,
692-
"\"}", NULL);
713+
// Note: this leverages the fact that C concats consecutive string literals together.
714+
mp_printf(&_socket_print,
715+
"{\"web_api_version\": 1, "
716+
"\"version\": \"" MICROPY_GIT_TAG "\", "
717+
"\"build_date\": \"" MICROPY_BUILD_DATE "\", "
718+
"\"board_name\": \"" MICROPY_HW_BOARD_NAME "\", "
719+
"\"mcu_name\": \"" MICROPY_HW_MCU_NAME "\", "
720+
"\"board_id\": \"" CIRCUITPY_BOARD_ID "\", "
721+
"\"creator_id\": %u, "
722+
"\"creation_id\": %u, "
723+
"\"hostname\": \"%s\", "
724+
"\"port\": %d, "
725+
"\"ip\": \"%s\"}", CIRCUITPY_CREATOR_ID, CIRCUITPY_CREATION_ID, hostname, web_api_port, _our_ip_encoded);
693726
// Empty chunk signals the end of the response.
694727
_send_chunk(socket, "");
695728
}
@@ -1179,7 +1212,12 @@ static void _process_request(socketpool_socket_obj_t *socket, _request *request)
11791212
request->authenticated = strncmp(request->header_value, prefix, strlen(prefix)) == 0 &&
11801213
strcmp(_api_password, request->header_value + strlen(prefix)) == 0;
11811214
} else if (strcasecmp(request->header_key, "Host") == 0) {
1182-
request->redirect = strcmp(request->header_value, "circuitpython.local") == 0;
1215+
// Do a prefix check so that port is ignored. Length must be the same or the
1216+
// header ends in :.
1217+
const char *cp_local = "circuitpython.local";
1218+
request->redirect = strncmp(request->header_value, cp_local, strlen(cp_local)) == 0 &&
1219+
(strlen(request->header_value) == strlen(cp_local) ||
1220+
request->header_value[strlen(cp_local)] == ':');
11831221
} else if (strcasecmp(request->header_key, "Content-Length") == 0) {
11841222
request->content_length = strtoul(request->header_value, NULL, 10);
11851223
} else if (strcasecmp(request->header_key, "Expect") == 0) {

0 commit comments

Comments
 (0)