Overview
PUT replaces a specified resource on the server as a whole (RESTful style). Its difference from POST is idempotency (no matter how many times you repeat the same operation, the result is the same): repeating PUT on the same resource always yields the same result (all replace it with identical content); while each POST adds a new record. Firmware version updates and configuration pushes commonly use PUT. This tutorial demonstrates: submitting firmware=1.2.3 via PUT to httpbin.org/put, and the server echoes the received data.
In plain words: PUT is replacing the "old-version document" on the server wholesale — like tearing the old poster off your wall and sticking on a new one (whole replacement, not appending). Unlike POST's "add a new record": repeatedly PUTting to the same address always gives the same result (idempotent), which is why it's perfect for "set the firmware version to 1.2.3"-style updates.
This tutorial is based on the official Ai-Thinker SDK (Ai-Thinker-Open/Ai-Thinker-WB2, version
release_bl_iot_sdk_1.6.40).⚠️ The official SDK does not ship a standalone PUT example project. This tutorial reworks the official example
applications/protocols/http_client_socket(the GET version); the only change is the request message (method line and request body) — the rest matches the official code, marked with comments.
The official SDK has no dedicated PUT project, so rework the official http_client_socket project directly:
cd ~/Ai-Thinker-WB2/applications/protocols/http_client_socket
Note:
cdis the “change directory” command, entering the official example project; all subsequentmakecommands must run in this directory.
💡 Copy the directory (e.g.
http_client_put) before modifying, keeping the official project untouched.
Open http_client_socket/main.c and change the SSID/password at the top (the official default is AIOT@FAE — change it to your own router).
Open http_client_socket/demo.c and change the target server:
#define WEB_SERVER "httpbin.org"
#define WEB_PORT "80"
#define WEB_PATH "/put"
💡
httpbin.org/putis a free PUT test endpoint — it echoes the received data as JSON. You can also use your own server.
Rework the request message to PUT based on the official demo.c. The full code for this step has been moved to the end of this page:
📜 Full Code — in the “Full Code” section below, collapsed by default — click to expand, matching the official example (
applications/protocols/http_client_socket/http_client_socket/demo.c) except the request message reworked to PUT per this page.
Code highlights (differences vs the official GET version):
| Change | Description |
|---|---|
PUT /put HTTP/1.0 |
Method line GET → PUT, path pointing at the resource-update endpoint |
Content-Type: application/x-www-form-urlencoded |
Tells the server what format the body is (key=value form); without stating it, the server can’t parse it |
Content-Length: 16 |
Must equal the actual body byte count (firmware=1.2.3 is 16 bytes); a wrong value hangs the request |
firmware=1.2.3 |
The request body (the new content to write into the resource); its byte count must match Content-Length |
| Everything else | Identical to the official http_client_socket |
💡 PUT vs POST: PUT is idempotent — repeating PUT to the same URL gives the same result (“set” semantics, e.g. updating a device firmware version); POST is non-idempotent — each execution produces a new effect (“add” semantics, e.g. reporting a new log). In RESTful APIs:
POST /devicescreates a device,PUT /devices/{id}updates a device.
Build in the project directory:
make -j8
Note:
makeis the “build” command, turning code into firmware (the program flashed into the board) the board can run;-j8builds with 8 parallel cores, faster.
On success a firmware build_out/http_client_socket.bin is generated.
Keep the board connected via USB, confirm the serial device, and flash:
make flash p=/dev/ttyUSB0 b=921600
Note:
make flashis the “flash” command, writing the compiled firmware into the board. Afterp=comes the serial device (often/dev/ttyUSB0on Linux,COM3-like on Windows — use your computer’s actual one),b=921600is the flash baud rate (serial transfer speed), keep the default.
⏳ During flashing, press and hold the EN button on the board when prompted to enter download mode; wait for the progress bar to complete — that means the flash succeeded.
After flashing, the board automatically restarts and runs. The serial (baud rate 921600) prints the connection logs, then httpbin.org’s JSON echo:
DNS lookup succeeded. IP=34.224.xxx.xxx
... connected
... socket send success
HTTP/1.1 200 OK
Content-Type: application/json
...
{
"args": {},
"data": "firmware=1.2.3",
"form": {
"firmware": "1.2.3"
},
...
}
... done reading from socket. Last read return=0 errno=0
HTTP/1.1 200 OK and "data": "firmware=1.2.3" appearing means the PUT succeeded — the server fully received the data. If you only see GOT IP without the echo, first confirm the board is online (GOT IP), your computer’s browser can open httpbin.org (target server reachable), then troubleshoot the message — see the FAQ at the end.
💡 Note the
formfield inhttpbin.org/put’s response shows the parsed key-value pairs; at the message level PUT and POST are nearly identical — the difference lies entirely in the semantics of the method line, which tells the server whether to “update” or “add”.
API Summary for This Tutorial
getaddrinfo(name, port, &hints, &res)
Resolves a domain name into a linked list of IPs.
Parameters:
name: domain or IP string, e.g."httpbin.org"port: port string, e.g."80"hints: query criteria structure (limits to IPv4 + TCP)res: output parameter, resolved linked-list pointer
Return: 0 on success; negative error code on failure
freeaddrinfo(res)
Frees the resolution result.
Parameters:
res: result linked list returned bygetaddrinfo
Return: none
socket(af, type, proto)
Creates a TCP socket.
Parameters:
af:AF_INET(IPv4)type:SOCK_STREAM(streaming TCP)proto: pass0
Return: socket descriptor on success; -1 on failure
connect(fd, addr, addrlen)
Connects to the server.
Parameters:
fd: socket descriptoraddr: server address (struct sockaddr*)addrlen: address length
Return: 0 on success; negative error code on failure
write(fd, buf, len)
Sends the whole HTTP message (including the request body).
Parameters:
fd: socket descriptorbuf: request message buffer (request line + headers + blank line + body)len: total message length
Return: bytes sent on success; negative on failure
read(fd, buf, len)
Reads the server response.
Parameters:
fd: socket descriptorbuf: receive bufferlen: buffer length
Return: bytes read; 0 = peer closed; negative = error
close(fd)
Closes the connection.
Parameters:
fd: socket descriptor
Return: 0 on success; -1 on failure
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, len)
Sets the receive timeout.
Parameters:
fd: socket descriptorlevel:SOL_SOCKEToptname:SO_RCVTIMEOtv:struct timevalpointerlen:sizeof(struct timeval)
Return: 0 on success; negative error code on failure
inet_ntoa(addr)
Converts an IP address to a string.
Parameters:
addr: network-byte-order IP
Return: dotted-decimal string
bl_putchar(c)
Outputs one character to the serial.
Parameters:
c: the character to output
Return: none
📌 The PUT message body format is identical to POST (
Content-Type+Content-Length+ body) — only the method line differs. Always updateContent-Lengthin sync after changing the body.
Full Code
Below is the complete demo.c source, matching the official example (applications/protocols/http_client_socket/http_client_socket/demo.c) except the request message reworked to PUT per this page:
📜 Click to expand the full demo.c code
#include <stdio.h>
#include <FreeRTOS.h>
#include <task.h>
#include <lwip/sockets.h>
#include <lwip/netdb.h>
#include <lwip/tcp.h>
#include <lwip/err.h>
#include <http_client.h>
#include <cli.h>
#include "demo.h"
#include <blog.h>
#define WEB_SERVER "httpbin.org"
#define WEB_PORT "80"
#define WEB_PATH "/put"
/* ★ Rework point ①: method GET → PUT, add Content-Type / Content-Length and request body */
static const char *REQUEST = "PUT " WEB_PATH " HTTP/1.0\r\n"
"Host: " WEB_SERVER ":" WEB_PORT "\r\n"
"User-Agent: aithinker wb2\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 16\r\n"
"\r\n"
"firmware=1.2.3"; /* ★ Rework point ②: request body, 16 bytes */
void http_get_task(void *pvParameters)
{
const struct addrinfo hints = {
.ai_family = AF_INET,
.ai_socktype = SOCK_STREAM,
};
struct addrinfo *res;
struct in_addr *addr;
int s, r;
char recv_buf[4096];
while (1)
{
int err = getaddrinfo(WEB_SERVER, "80", &hints, &res);
if (err != 0 || res == NULL)
{
blog_error("DNS lookup failed err=%d res=%p", err, res);
vTaskDelay(1000 / portTICK_PERIOD_MS);
continue;
}
addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
blog_info("DNS lookup succeeded. IP=%s", inet_ntoa(*addr));
s = socket(res->ai_family, res->ai_socktype, 0);
if (s < 0)
{
blog_error("... Failed to allocate socket.");
freeaddrinfo(res);
vTaskDelay(1000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... allocated socket");
if (connect(s, res->ai_addr, res->ai_addrlen) != 0)
{
blog_error("... socket connect failed errno=%d", errno);
close(s);
freeaddrinfo(res);
vTaskDelay(4000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... connected");
freeaddrinfo(res);
if (write(s, REQUEST, strlen(REQUEST)) < 0)
{
blog_error("... socket send failed");
close(s);
vTaskDelay(4000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... socket send success");
struct timeval receiving_timeout;
receiving_timeout.tv_sec = 5;
receiving_timeout.tv_usec = 0;
if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &receiving_timeout,
sizeof(receiving_timeout)) < 0)
{
blog_error("... failed to set socket receiving timeout");
close(s);
vTaskDelay(4000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... set socket receiving timeout success");
// FIXME fix putchar
extern int bl_putchar(int c);
/* Read HTTP response */
do
{
bzero(recv_buf, sizeof(recv_buf));
r = read(s, recv_buf, sizeof(recv_buf) - 1);
for (int i = 0; i < r; i++)
{
bl_putchar(recv_buf[i]);
}
} while (r > 0);
blog_info("... done reading from socket. Last read return=%d errno=%d\r\n", r, errno);
close(s);
for (int countdown = 10; countdown >= 0; countdown--)
{
blog_info("%d... ", countdown);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
blog_info("Starting again!");
}
}FAQ & Troubleshooting
⚠️ Server returns 405 Method Not Allowed
Cause: the target path doesn't support the PUT method
Fix: use a PUT-capable endpoint (httpbin.org/put, httpbin.org/anything) or your own server
⚠️ Server receives an empty body / request hangs
Cause: Content-Length doesn't match the actual body byte count
Fix: after changing the body, count the bytes one by one (firmware=1.2.3 is 16 bytes) and keep Content-Length in sync
⚠️ Can't tell PUT from POST
Cause: at the message level the two are nearly identical, differing only in the method line and semantics
Fix: use POST for add/report (non-idempotent), PUT for update/replace (idempotent); the API docs will state which methods each endpoint supports
⚠️ Other issues (can't connect to router / DNS failure / timeout / stack overflow)
Note: identical to HTTP GET
Fix: follow the pitfalls in HTTP GET (including can't connect to the router, serial not found, flashing failure, etc.)
⚠️ Serial device not found / can't open
Cause: USB-to-serial driver not installed, insufficient permission, or the cable only charges and can't transfer data
Fix: on Linux check the device with lsusb/dmesg; if permission denied run sudo chmod 666 /dev/ttyUSB0; on Windows install the driver and check the COM port in Device Manager; try a data-capable cable
⚠️ Flashing keeps waiting / fails
Cause: download mode wasn't entered, wrong baud rate, or a wrong serial number
Fix: press and hold EN during flashing to enter download mode as prompted; change p=/dev/ttyUSB0 to your actual serial port; try another USB port or cable
Self-Check
"data": "firmware=1.2.3" appears in the JSON echo on the serial — the PUT submission is verified.

