{"id":7939,"date":"2022-08-29T09:09:41","date_gmt":"2022-08-29T16:09:41","guid":{"rendered":"https:\/\/visualgdb.com\/w\/?p=7939"},"modified":"2022-09-06T08:18:31","modified_gmt":"2022-09-06T15:18:31","slug":"using-the-socket-api-on-raspberry-pi-w-with-freertos","status":"publish","type":"post","link":"https:\/\/visualgdb.com\/tutorials\/raspberry\/pico_w\/freertos\/","title":{"rendered":"Using the socket API on Raspberry Pi W with FreeRTOS"},"content":{"rendered":"<p>This tutorial shows how to create a basic TCP server for the Raspberry Pi Pico W board using FreeRTOS and the lwIP socket API.<\/p>\n<p>The use of FreeRTOS adds minimal overhead due to switching between multiple threads, however it allows using the classical socket API with one thread per connection and eliminates the need to split the high-level logic between multiple callback functions invoked in the same thread.<\/p>\n<p>In this tutorial we will clone the <strong>FreeRTOS iperf<\/strong> example from the Raspberry Pi Pico SDK and will update it to work as a generic TCP server.<\/p>\n<p>Before you begin, install VisualGDB 5.6R8 or later.<\/p>\n<ol>\n<li>Start Visual Studio and locate the Raspberry Pi Pico project wizard:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/01-newprj-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7940\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/01-newprj-1.png\" alt=\"\" width=\"1024\" height=\"680\" \/><\/a><\/li>\n<li>Enter the name and location for your project: <a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/02-freertos.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7941\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/02-freertos.png\" alt=\"\" width=\"1024\" height=\"680\" \/><\/a><\/li>\n<li>Proceed with creating a new project as shown below: <a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/03-new-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7942\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/03-new-1.png\" alt=\"\" width=\"856\" height=\"693\" \/><\/a><\/li>\n<li>Select your ARM toolchain, the Pico SDK 1.4.0+ and enter your Wi-Fi network settings: <a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/04-sdk-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7943\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/04-sdk-1.png\" alt=\"\" width=\"856\" height=\"693\" \/><\/a>If you are not sure about the Wi-Fi setup, check out <a href=\"https:\/\/visualgdb.com\/tutorials\/raspberry\/pico_w\">this tutorial<\/a> for more details.<\/li>\n<li>Select the <strong>freertos\/iperf<\/strong> example and click &#8220;Next&#8221;:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/05-iperf.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7944\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/05-iperf.png\" alt=\"\" width=\"856\" height=\"693\" \/><\/a><\/li>\n<li>Connect your Raspberry Pi Pico W to an SWD debug probe and select the matching settings on the Debug Method page:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/07-debug.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7931\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/07-debug.png\" alt=\"\" width=\"856\" height=\"693\" \/><\/a>If this is the first time you are debugging this Pico W board, see <a href=\"https:\/\/visualgdb.com\/documentation\/embedded\/raspberry\/pico\/swd\/\">this page<\/a> for detailed instructions on getting SWD working.<\/li>\n<li>Press &#8220;Finish&#8221; to create the project. Note that the original <strong>iperf<\/strong> sample build 2 executables: <strong>pico_w_freertos_iperf_server_nosys<\/strong> and <strong>picow_freertos_iperf_server_sys<\/strong>. The difference between them is the lwIP <a href=\"https:\/\/www.nongnu.org\/lwip\/2_0_x\/group__lwip__opts__nosys.html\">NO_SYS<\/a> macro, that removes FreeRTOS-specific support from lwIP, precluding the use of the regular socket API. To avoid any confusion, click on the <strong>pico_w_freertos_iperf_server_nosys<\/strong> target in Solution Explorer and press the <strong>Delete<\/strong> key to remove it:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/07-nosys-del.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7945\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/07-nosys-del.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a><\/li>\n<li>Note that VisualGDB will automatically clean up the regular CMake statements like <strong>add_executable()<\/strong> but not the Pico SDK-specific <strong>pico_add_extra_output()<\/strong> statement, so the project will refuse to load. Click on the last line in the CMake output to navigate to the line with the <strong>pico_add_extra_outputs()<\/strong> statement:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/08-outputs-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7949\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/08-outputs-1.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a><\/li>\n<li>Once you remove the statement and save the CMakeLists.txt file, the project will get loaded properly and the <strong>nosys<\/strong> executable will disappear: <a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/09-sys.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7950\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/09-sys.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a><\/li>\n<li>You can also change the name of the main source file and the main executable to <strong>tcp_demo<\/strong> via Solution Explorer, however it may require a couple of manual adjustments:\n<ol style=\"list-style-type: lower-alpha;\">\n<li>VisualGDB will automatically update the <strong>target_compile_definitions()<\/strong>, <strong>target_include_directories()<\/strong> and <strong>target_link_libraries()<\/strong> statements, but not the PicoSDK-specific <strong>pico_add_extra_outputs()<\/strong> statement. The latter will need to be updated manually.<\/li>\n<li>If you are using an older VisualGDB version, renaming the executable may insert a space after the <strong>WIFI_SSID<\/strong>\/<strong>WIFI_PASSWORD<\/strong> definitions, preventing the build from succeeding. If this happens, simply remove the spaces and the project will build as expected.<\/li>\n<\/ol>\n<p><a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/10-rename.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7951\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/10-rename.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a><\/li>\n<li>Replace the contents of the main source file with the following code:\n<pre class=\"\">#include \"pico\/cyw43_arch.h\"\r\n#include \"pico\/stdlib.h\"\r\n\r\n#include \"lwip\/apps\/lwiperf.h\"\r\n#include \"lwip\/ip4_addr.h\"\r\n#include \"lwip\/netif.h\"\r\n\r\n#include \"FreeRTOS.h\"\r\n#include \"task.h\"\r\n\r\n#define TEST_TASK_PRIORITY (tskIDLE_PRIORITY + 2UL)\r\n\r\nstatic void run_server()\r\n{\r\n    while (true)\r\n    {\r\n        vTaskDelay(100);\r\n    }\r\n}\r\n\r\nstatic void main_task(__unused void *params)\r\n{\r\n    if (cyw43_arch_init())\r\n    {\r\n        printf(\"failed to initialise\\n\");\r\n        return;\r\n    }\r\n\r\n    cyw43_arch_enable_sta_mode();\r\n\r\n    printf(\"Connecting to WiFi...\\n\");\r\n\r\n    if (cyw43_arch_wifi_connect_timeout_ms(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, 30000))\r\n    {\r\n        printf(\"failed to connect.\\n\");\r\n        exit(1);\r\n    }\r\n    else\r\n    {\r\n        printf(\"Connected.\\n\");\r\n    }\r\n\r\n    run_server();\r\n    cyw43_arch_deinit();\r\n}\r\n\r\nint main(void)\r\n{\r\n    stdio_init_all();\r\n    TaskHandle_t task;\r\n    xTaskCreate(main_task, \"MainThread\", configMINIMAL_STACK_SIZE, NULL, TEST_TASK_PRIORITY, &amp;task);\r\n    vTaskStartScheduler();\r\n}<\/pre>\n<p>This removes the logic for launching iperf, running lwIP without FreeRTOS integration, and starting the iperf client\/server.<\/li>\n<li>If you try running this version of the code, it will fail with a SIGTRAP error indicating memory corruption. To find it out, check the <strong>Debug-&gt;Windows-&gt;Live Watch-&gt;FreeRTOS-&gt;Threads<\/strong> (requires VisualGDB Custom Edition or higher):<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/12-stack.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7953\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/12-stack.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a>Note how the highest stack usage for the MainThread has almost exceeded the maximum value, and the maximum usage for\u00a0 <strong>CYW43 Worker<\/strong> thread cannot be computed, indicating that the stack has overflown.<\/li>\n<li>If you search for the &#8220;<strong>CYW43 Worker<\/strong>&#8221; text in the entire project, you can find how the thread is created with the minimal stack size:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/13-size.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7954\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/13-size.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a><\/li>\n<li>Increase the stack size from 256 to 512 by editing the <strong>configMINIMAL_STACK_SIZE<\/strong> definition in <strong>FreeRTOSConfig.h<\/strong>. You can also edit the <strong>configTOTAL_HEAP_SIZE<\/strong> value to increase the size of the heap:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/14-newsize.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7955\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/14-newsize.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a><\/li>\n<li>Now it&#8217;s time to add logic for creating the sockets. Replace the previous version of <strong>run_server()<\/strong> with the following code:\n<pre class=\"\">#include &lt;lwip\/sockets.h&gt;\r\n\r\nstatic int handle_single_command(int conn_sock)\r\n{\r\n    vTaskDelay(100);\r\n    return 0;\r\n}\r\n\r\nstatic void handle_connection(int conn_sock)\r\n{\r\n    while (!handle_single_command(conn_sock))\r\n    {\r\n    }\r\n\r\n    closesocket(conn_sock);\r\n}\r\n\r\nstatic void run_server()\r\n{\r\n    int server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);\r\n    struct sockaddr_in listen_addr =\r\n        {\r\n            .sin_len = sizeof(struct sockaddr_in),\r\n            .sin_family = AF_INET,\r\n            .sin_port = htons(1234),\r\n            .sin_addr = 0,\r\n        };\r\n\r\n    if (server_sock &lt; 0)\r\n    {\r\n        printf(\"Unable to create socket: error %d\", errno);\r\n        return;\r\n    }\r\n\r\n    if (bind(server_sock, (struct sockaddr *)&amp;listen_addr, sizeof(listen_addr)) &lt; 0)\r\n    {\r\n        printf(\"Unable to bind socket: error %d\\n\", errno);\r\n        return;\r\n    }\r\n\r\n    if (listen(server_sock, 1) &lt; 0)\r\n    {\r\n        printf(\"Unable to listen on socket: error %d\\n\", errno);\r\n        return;\r\n    }\r\n\r\n    printf(\"Starting server at %s on port %u\\n\", ip4addr_ntoa(netif_ip4_addr(netif_list)), ntohs(listen_addr.sin_port));\r\n\r\n    while (true)\r\n    {\r\n        struct sockaddr_storage remote_addr;\r\n        socklen_t len = sizeof(remote_addr);\r\n        int conn_sock = accept(server_sock, (struct sockaddr *)&amp;remote_addr, &amp;len);\r\n        if (conn_sock &lt; 0)\r\n        {\r\n            printf(\"Unable to accept incoming connection: error %d\\n\", errno);\r\n            return;\r\n        }\r\n        handle_connection(conn_sock);\r\n    }\r\n}<\/pre>\n<\/li>\n<li>The code will initially not compile because the socket API is not enabled in the Raspberry Pi Pico SDK by default. Locate the <strong>lwipopts_examples_common.h<\/strong> file in the project directory and set the <strong>LWIP_SOCKET<\/strong> definition to 1:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/11-sock.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7952\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/11-sock.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a><\/li>\n<li>If you try running this version of the code, the lwIP framework will trigger an exception when trying to create the socket. Going up the call stack will show that the <strong>netconn_alloc()<\/strong> function is trying to allocate a zero-sized queue because the <strong>DEFAULT_TCP_RECVMBOX_SIZE<\/strong> macro is set to 0:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/16-error-2.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7959\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/16-error-2.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a><\/li>\n<li>In order to fix it, open the <strong>lwipopts.h<\/strong> file in the project directory and add the following code there:\n<pre class=\"\">#define DEFAULT_UDP_RECVMBOX_SIZE TCPIP_MBOX_SIZE\r\n#define DEFAULT_TCP_RECVMBOX_SIZE TCPIP_MBOX_SIZE\r\n#define DEFAULT_ACCEPTMBOX_SIZE TCPIP_MBOX_SIZE<\/pre>\n<p>Now the socket functions will work as expected:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/17-mbox.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7960\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/17-mbox.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a><\/li>\n<li>Finally, we can replace the empty <strong>handle_single_command() <\/strong>implementation with a more meaningful one:\n<pre class=\"\">static void send_message(int socket, char *msg)\r\n{\r\n    int len = strlen(msg);\r\n    int done = 0;\r\n    while (done &lt; len)\r\n    {\r\n        int done_now = send(socket, msg + done, len - done, 0);\r\n        if (done_now &lt;= 0)\r\n            return;\r\n        done += done_now;\r\n    }\r\n}\r\n\r\nstatic int handle_single_command(int conn_sock)\r\n{\r\n    char buffer[128];\r\n    int done = 0;\r\n    send_message(conn_sock, \"Enter command: \");\r\n\r\n    while (done &lt; sizeof(buffer))\r\n    {\r\n        int done_now = recv(conn_sock, buffer + done, sizeof(buffer) - done, 0);\r\n        if (done_now &lt;= 0)\r\n            return -1;\r\n        done += done_now;\r\n        char *end = strnstr(buffer, \"\\r\", done);\r\n        if (!end)\r\n            continue;\r\n        *end = 0;\r\n\r\n        if (!strcmp(buffer, \"on\"))\r\n        {\r\n            cyw43_arch_gpio_put(0, true);\r\n            send_message(conn_sock, \"The LED is now on\\r\\n\");\r\n        }\r\n        else if (!strcmp(buffer, \"off\"))\r\n        {\r\n            cyw43_arch_gpio_put(0, false);\r\n            send_message(conn_sock, \"The LED is now off\\r\\n\");\r\n        }\r\n        else\r\n        {\r\n            send_message(conn_sock, \"Unknown command\\r\\n\");\r\n        }\r\n        break;\r\n    }\r\n\r\n    return 0;\r\n}<\/pre>\n<p>It will ask the user to enter a command, and will then expect either the &#8220;<strong>on<\/strong>&#8221; command for turning on the LED, or &#8220;<strong>off<\/strong>&#8221; for turning it off.<\/li>\n<li>Before you run this code, we advise opening VisualGDB Project Properties and referencing the <strong>Fast Semihosting and Embedded Profiler<\/strong> framework so that you can see the output from <strong>printf()<\/strong> without having to connect to the UART pins:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/18-profiler.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7961\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/18-profiler.png\" alt=\"\" width=\"982\" height=\"695\" \/><\/a><\/li>\n<li>Try running the program now. It will connect to the Wi-Fi network and will display the IP address it acquired:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/19-ip.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7962\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/19-ip.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a><\/li>\n<li>Try connecting to the shown IP address via telnet (<strong>Start-&gt;Run-&gt;telnet &lt;IP&gt; 1234<\/strong>) and running some commands. Observe how the LED turns on and off:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/20-commandloop.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7963\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/20-commandloop.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a>Note that opening subsequent telnet connections won&#8217;t work until the first connection is closed because the code still handles them within one thread.<\/li>\n<li>This limitation can be easily avoided if we create a separate thread for each incoming connection. First of all, create a semaphore that will limit the amount of simultaneously running connection threads:\n<pre class=\"\">const int kConnectionThreadCount = 3;\r\nstatic xSemaphoreHandle s_ConnectionSemaphore;\r\n\r\nint main()\r\n{\r\n    \/* .... *\/\r\n    s_ConnectionSemaphore = xSemaphoreCreateCounting(kConnectionThreadCount, kConnectionThreadCount);\r\n    vTaskStartScheduler();\r\n}<\/pre>\n<p>Then update the <strong>handle_connection()<\/strong> function to start a new thread instead of running in-place:<\/p>\n<pre class=\"\">static void do_handle_connection(void *arg)\r\n{\r\n    int conn_sock = (int)arg;\r\n    while (!handle_single_command(conn_sock))\r\n    {\r\n    }\r\n\r\n    closesocket(conn_sock);\r\n    xSemaphoreGive(s_ConnectionSemaphore);\r\n    vTaskDelete(NULL);\r\n}\r\n\r\nstatic void handle_connection(int conn_sock)\r\n{\r\n    TaskHandle_t task;\r\n    xSemaphoreTake(s_ConnectionSemaphore, portMAX_DELAY);\r\n    xTaskCreate(do_handle_connection, \"Connection Thread\", configMINIMAL_STACK_SIZE, (void *)conn_sock, TEST_TASK_PRIORITY, &amp;task);\r\n}<\/pre>\n<p>Finally, increase the backlog size passed to the listen() function in run_server():<\/p>\n<pre class=\"\">if (listen(server_sock, kConnectionThreadCount * 2) &lt; 0)\r\n{\r\n    printf(\"Unable to listen on socket: error %d\\n\", errno);\r\n    return;\r\n}<\/pre>\n<\/li>\n<li>Now you will be able to handle up to <strong>kConnectionThreadCount<\/strong> connections simultaneously without having to manually pass the state between callback functions:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/21-multiconn.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7964\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2022\/08\/21-multiconn.png\" alt=\"\" width=\"1230\" height=\"841\" \/><\/a>You can use the Live Watch view to monitor the value of the <strong>s_ConnectionSemaphore<\/strong> semaphore in the real time. It will reflect the number of currently running connection threads.<\/li>\n<\/ol>\n<p>You can find the source code for the project shown in this tutorial in our <a href=\"https:\/\/github.com\/sysprogs\/tutorials\/tree\/master\/visualgdb\/ARM\/PicoSDK\/PicoW\/FreeRTOSDemo\">GitHub repository<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This tutorial shows how to create a basic TCP server for the Raspberry Pi Pico W board using FreeRTOS and<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[18],"tags":[231,102],"_links":{"self":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts\/7939"}],"collection":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/comments?post=7939"}],"version-history":[{"count":2,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts\/7939\/revisions"}],"predecessor-version":[{"id":7966,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts\/7939\/revisions\/7966"}],"wp:attachment":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/media?parent=7939"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/categories?post=7939"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/tags?post=7939"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}