Skip to content
Shop

Save temperature and humidity to Google Sheets with AC09 Cat-M1 TY (STM32)

Explore with your favorite AI

Gemini & other AI

Copy this page’s context into your AI to explore how it works and what you can build.

Open Gemini

Copy the prompt, then paste it into Gemini’s message box.

View prompt

This example uses the AC09 Cat-M1 TY leaf to send temperature and humidity measured by the 4-Sensors leaf to Google Sheets every 10 minutes.

The STM32 leaf reads temperature and humidity, and AC09 Cat-M1 TY sends the data over a Cellular LPWA Cat-M1 network. You can check the saved measurements in Google Sheets.

The intended configuration runs from a battery connected to AV06 with USB disconnected. After programming, the firmware automatically waits at startup until it can obtain the time from the modem, then starts sending temperature and humidity. Entering the time from a computer is not required.

Temperature and humidity data follow this path:

flowchart TD
    sensor["4-Sensors<br/>Temperature and humidity"]
    mcu["STM32 MCU<br/>Read and send data"]
    lte["AC09 Cat-M1 TY"]
    network["Cellular LPWA Cat-M1 network"]
    receiver["Google Apps Script (GAS)<br/>HTTP endpoint"]
    sheet["Google Sheets"]

    sensor --> mcu --> lte --> network
    network -->|HTTPS POST / JSON| receiver
    receiver -->|Append received time, temperature, humidity| sheet

The temperature and humidity are sent as JSON to a GAS web app. GAS checks the token and measurements, then appends them to the spreadsheet. The connection to the HTTP endpoint uses HTTPS.

After startup, once the network connection and time are available, the firmware reads and sends the first temperature and humidity values. It then starts a new reading and transmission every 10 minutes. If connection or response handling takes more than 10 minutes, that cycle is skipped and the next reading and transmission start 10 minutes after processing finishes. The arrival interval at the sheet is not guaranteed to remain exactly 10 minutes, including during reconnection after communication failures.

Use the following leaves. The table lists them from top to bottom in the assembled stack.

PositionLeafRole in this example
1 (top)AC09 Cat-M1 TYSends temperature and humidity over Cellular LPWA Cat-M1
2AX04 SpacerPlaced between AC09 and 4-Sensors
3AI01 4-SensorsMeasures temperature and humidity using its onboard sensor
4AP03 STM32 MCUReads and sends the measurements
5AZ01C USB-CConnects to a computer for programming and serial communication
6 (bottom)AV06 1.8V–5.5VSupplies power to the leaves from the connected battery
ItemPreparation
Google AccountRequired to use Google Sheets to store temperature and humidity
Development computerSet up the STM32 development environment using the environment guide
USB-C cableUse a data-capable cable for programming and checking logs. Disconnect it for battery operation
BatteryPrepare a battery and cable that meet the AV06 power and connector specifications
SIM cardPrepare one of the SIMs listed below

Use one of the following:

  • The 1NCE SIM included with the Cellular LPWA Cat-M1 leaf
  • A 1NCE SIM you have contracted yourself
  • A SORACOM SIM (plan-D) you have contracted yourself

If you need a new SIM contract, expand the relevant section below. AC09 Cat-M1 TY uses a nanoSIM.

Obtain a 1NCE SIM

These steps describe purchasing a SIM directly from 1NCE. Direct contracts for the 1NCE service in Japan are available only to corporations. Check the official service information (Japanese) for contract terms.

  1. Open the online shop.
    Go to the shop from the 1NCE Japan website and check that the billing country is Japan.
  2. Choose the SIM type and quantity.
    Select the physical “IoT SIM Card Business (3-in-1).” This example needs one SIM. After delivery, detach the nano-size SIM from the 3-in-1 card.
  3. Create an account.
    Follow the prompts and enter the company, contact, shipping, and other information required for the order. Sign in if you already have an account.
  4. Complete the order and payment.
    Check the SIM type, quantity, shipping address, and price, then pay using a method shown on the screen.
  5. Check the SIM and management portal.
    After delivery, sign in to the 1NCE Customer Portal and check the purchased SIM and its status.

See 1NCE’s contact and purchase information (Japanese) and the official IoT SIM Card Business documentation for purchasing information and SIM specifications.

Obtain a SORACOM SIM (plan-D)

These steps describe ordering a plan-D nanoSIM through the SORACOM User Console. For a new purchase, choose the plan-D (D-500MB) data-only nano-size SIM (Japanese).

  1. Create a SORACOM account.
    Open registration from the official account creation guide (Japanese), then select individual or corporate use, country, and the Japan coverage type. Set an email address and password, verify the email address, and complete the subscriber information. Sign in if you already have an account.
  2. Register a payment method.
    Sign in to the SORACOM User Console, open your username at the upper right, and choose payment settings (支払い設定). For credit card payment, use the option to register a new credit card (+新しいクレジットカードを登録).
  3. Start a new order.
    Open Menu (メニュー) → Orders (発注) → New order (+新規注文). Select the SIM cards (SIMカード) tab for Japan coverage.
  4. Choose a plan-D nanoSIM.
    Select plan-D (D-500MB), data only (データ通信のみ), nano size (ナノサイズ), and quantity 1. Choose shipping, check the total quantity and price, and continue.
  5. Enter the shipping address and place the order.
    Register or select the destination, review the order and terms, then confirm the order (注文を確定する). See the official SIM ordering guide (Japanese) for screen details.
  6. Confirm delivery after the SIM arrives.
    Open the order under Menu (メニュー) → Orders (発注) in the User Console and confirm receipt (受け取り確認).
  7. Check registration under SIM management.
    Open Menu (メニュー) → SORACOM Air for Cellular (SORACOM Air for セルラー) → SIM management (SIM管理), and check that the SIM appears for Japan coverage. See the official SIM registration guide (Japanese) for delivery confirmation and registration details.

Before programming the example, set up the STM32 development environment on your computer. Follow the STM32 MCU instructions in the environment guide.

This example uses PlatformIO and the STM32 Arduino framework. The project includes the AP03 board definition and pin configuration. PlatformIO obtains the TLS and temperature/humidity sensor libraries during the first build.

Stack the leaves in the following order from top to bottom. Place the Spacer leaf between AC09 Cat-M1 TY and 4-Sensors.

Top
AC09 Cat-M1 TY
AX04 Spacer
AI01 4-Sensors
AP03 STM32 MCU
AZ01C USB-C
AV06
Bottom

Create an HTTP endpoint in Google Apps Script (GAS) to receive temperature and humidity. Leafony sends an HTTPS POST request to this endpoint, and GAS appends the received values to Google Sheets.

The temperature sheet stores three columns. Each transmission adds one row. The code uses the following Japanese column headings.

ColumnHeadingStored value
A受信日時(UTC)UTC date and time when GAS writes the data, for example 2026-09-18T03:00:00.000Z
B温度(℃)Numeric temperature measured by 4-Sensors, for example 25.5
C湿度(%RH)Numeric relative humidity measured by 4-Sensors, for example 50.0

Humidity is relative humidity (%RH). The transmitted JSON uses temperature_c for temperature and humidity_pct for humidity.

The timestamp is Google’s reception time, not the measurement time. The final Z indicates UTC. Japan Standard Time is UTC plus nine hours. Leafony’s transmission interval is 10 minutes, but communication time affects the interval between timestamps recorded in the sheet.

  1. Sign in to your Google Account and create a new spreadsheet in Google Sheets. For example, name it “Leafony temperature and humidity log.”

  2. Copy the spreadsheet ID from the browser URL. It is the SPREADSHEET_ID portion below; do not include the trailing gid.

    https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit
  3. Open Extensions → Apps Script in the spreadsheet. Name the GAS project, for example, “Leafony temperature and humidity receiver.”

See Google’s bound-script guide for creating a GAS project from a spreadsheet. This example opens the destination by spreadsheet ID so the web app can also identify it.

Replace the contents of the editor’s script file (コード.gs in the Japanese UI) with the code below and save it. doPost(e) reads the JSON body, checks the token and measurements, then appends a row. doGet() does not write data.

GAS code

const SHEET_NAME = 'temperature';
const HEADERS = ['受信日時(UTC)', '温度(℃)', '湿度(%RH)'];
// スクリプト プロパティを設定した後、エディターから一度実行します。
function setup() {
const config = readConfiguration();
if (!config) {
throw new Error('API_TOKEN と SPREADSHEET_ID のスクリプト プロパティを確認してください。');
}
const lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) {
throw new Error('処理中です。少し待って setup を再実行してください。');
}
try {
const spreadsheet = SpreadsheetApp.openById(config.spreadsheetId);
const sheet = spreadsheet.getSheetByName(SHEET_NAME)
|| spreadsheet.insertSheet(SHEET_NAME);
if (sheet.getLastRow() === 0) {
sheet.appendRow(HEADERS);
} else if (!hasExpectedHeader(sheet)) {
// 温度のみの旧シートは、C 列以降に値や数式がない場合だけ移行します。
const header = sheet.getRange(1, 1, 1, 2).getValues()[0];
if (header[0] === HEADERS[0] && header[1] === HEADERS[1]
&& sheet.getLastColumn() <= 2) {
sheet.getRange(1, 3).setValue(HEADERS[2]);
} else {
throw new Error('temperature シートの先頭行を確認してください。既存のデータは変更していません。');
}
}
SpreadsheetApp.flush();
} finally {
lock.releaseLock();
}
}
function doPost(e) {
try {
const body = e && e.postData && e.postData.contents;
if (typeof body !== 'string' || body.length === 0) {
return jsonResponse({ ok: false, error: 'invalid_request' });
}
// length は受信本文のバイト数。文字数も確認して過大な本文を解析しません。
if (e.postData.length > 1024 || e.contentLength > 1024 || body.length > 1024) {
return jsonResponse({ ok: false, error: 'body_too_large' });
}
let data;
try {
data = JSON.parse(body);
} catch (_) {
return jsonResponse({ ok: false, error: 'invalid_json' });
}
if (!data || typeof data !== 'object' || Array.isArray(data)) {
return jsonResponse({ ok: false, error: 'invalid_request' });
}
const config = readConfiguration();
if (!config) {
return jsonResponse({ ok: false, error: 'not_configured' });
}
if (typeof data.token !== 'string' || data.token !== config.token) {
return jsonResponse({ ok: false, error: 'invalid_token' });
}
if (typeof data.temperature_c !== 'number'
|| !Number.isFinite(data.temperature_c)
|| data.temperature_c < -40 || data.temperature_c > 120) {
return jsonResponse({ ok: false, error: 'invalid_temperature' });
}
if (typeof data.humidity_pct !== 'number'
|| !Number.isFinite(data.humidity_pct)
|| data.humidity_pct < 0 || data.humidity_pct > 100) {
return jsonResponse({ ok: false, error: 'invalid_humidity' });
}
const lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) {
return jsonResponse({ ok: false, error: 'busy' });
}
try {
const sheet = SpreadsheetApp.openById(config.spreadsheetId)
.getSheetByName(SHEET_NAME);
if (!sheet || !hasExpectedHeader(sheet)) {
return jsonResponse({ ok: false, error: 'sheet_not_initialized' });
}
sheet.appendRow([new Date().toISOString(), data.temperature_c, data.humidity_pct]);
SpreadsheetApp.flush();
return jsonResponse({ ok: true });
} finally {
lock.releaseLock();
}
} catch (_) {
// 例外の詳細や設定値を HTTP 応答に含めません。
return jsonResponse({ ok: false, error: 'internal_error' });
}
}
function doGet() {
return jsonResponse({ ok: false, error: 'method_not_allowed' });
}
function readConfiguration() {
const properties = PropertiesService.getScriptProperties();
const token = properties.getProperty('API_TOKEN');
const spreadsheetId = properties.getProperty('SPREADSHEET_ID');
if (!token || token.length < 32 || token.length > 128 || /[^A-Za-z0-9_-]/.test(token)
|| !spreadsheetId || !spreadsheetId.trim()) {
return null;
}
return { token: token, spreadsheetId: spreadsheetId.trim() };
}
function hasExpectedHeader(sheet) {
if (sheet.getLastRow() === 0) return false;
const header = sheet.getRange(1, 1, 1, 3).getValues()[0];
return HEADERS.every((value, index) => header[index] === value);
}
function jsonResponse(value) {
return ContentService.createTextOutput(JSON.stringify(value))
.setMimeType(ContentService.MimeType.JSON);
}

See the official web app guide for receiving POST requests and the appendRow() reference for appending rows.

Open Project Settings on the left of the GAS editor. Add and save these two Script Properties:

PropertyValue
SPREADSHEET_IDThe spreadsheet ID copied earlier
API_TOKENA token generated for this example, 32–128 characters long, containing only letters, digits, hyphens, and underscores

Run this command in a computer terminal to generate a random 64-character hexadecimal token. Set the displayed string as API_TOKEN and later use the same value in Leafony’s configuration.

Terminal window
openssl rand -hex 32

Project editors can also read script properties. See Google’s property-management instructions for the settings screen.

  1. Return to the GAS editor and select setup in the function selector at the top.
  2. Click Run. On the first run, select your Google Account and grant the script access to the spreadsheet.
  3. Return to the spreadsheet and check that the temperature sheet and first-row headings “受信日時(UTC)”, “温度(℃)”, and “湿度(%RH)” have been created.

Running setup() once is sufficient. Running it again does not delete recorded data. If a sheet with the same name has different headings or data, it stops with an error without changing them. Rename the existing sheet and run it again in that case.

When upgrading from the older version that stored only temperature, replace the GAS code and run setup() again. It adds “湿度(%RH)” to C1 only if the first two headings match the older version and there are no values or formulas in column C or later. Existing dates and temperatures remain; humidity in past rows stays blank. If column C or later contains data, it does not change the sheet automatically.

Then update the GAS deployment as described below and program the firmware that supports humidity. The new GAS code requires humidity_pct, so it does not accept the older temperature-only payload.

  1. Open Deploy → New deployment at the upper right of the GAS editor.
  2. Under Select type, choose Web app.
  3. Set Execute as to Me.
  4. Set Who has access to Anyone. Leafony does not sign in to a Google Account, so anonymous access must be allowed. If your Google Workspace settings prevent choosing Anyone, contact your organization’s administrator.
  5. Click Deploy and grant the access required by this script if prompted.
  6. Copy the displayed Web app URL. Check that it ends in /exec. This is Leafony’s destination URL.
https://script.google.com/macros/s/DEPLOYMENT_ID/exec

Follow Google’s web app deployment instructions to set access and obtain the URL. Test URLs ending in /dev are limited to users with edit access, so configure Leafony with the /exec URL.

After changing GAS code, open Deploy → Manage deployments, edit the existing deployment, select a new version, and deploy again. Updating the same deployment preserves the destination URL. See how to update a deployment.

First send one test temperature and humidity reading from a computer. Replace the entire URL containing YOUR_DEPLOYMENT_ID below with the copied web app URL. Also replace YOUR_API_TOKEN with your token.

  1. Search for PowerShell in the Start menu and open Windows PowerShell or PowerShell.
  2. Run curl.exe --version and check that version information appears. If the command is not found, see the official curl downloads for Windows.
  3. Replace the URL and token below with your values, then paste and run the entire block in PowerShell.
Terminal window
$url = 'https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec'
$payload = '{"token":"YOUR_API_TOKEN","temperature_c":25.5,"humidity_pct":50.0}'
$requestFile = [System.IO.Path]::GetTempFileName()
try {
Set-Content -LiteralPath $requestFile -Value $payload -Encoding Ascii -ErrorAction Stop
curl.exe -L $url -H 'Content-Type: application/json' --data-binary "@$requestFile"
} finally {
Remove-Item -LiteralPath $requestFile
}

In Windows PowerShell, curl is an alias for a different command, so use curl.exe explicitly. The example saves the JSON to a temporary file to preserve its quotation marks, sends the file, then deletes it. It works with Windows PowerShell 5.1 and PowerShell 7. See using curl on Windows.

Run the following command in a terminal:

Terminal window
curl -L \
'https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec' \
-H 'Content-Type: application/json' \
--data '{"token":"YOUR_API_TOKEN","temperature_c":25.5,"humidity_pct":50.0}'

On success, the following JSON is returned. Also check that a row containing the reception time, temperature 25.5, and humidity 50.0 has been added to the temperature sheet.

{"ok":true}

--data or --data-binary makes the initial request a POST. GAS ContentService redirects the response to a different URL; -L follows it and retrieves the final result with GET. You do not need to add -X POST. See ContentService redirects.

Do not judge success from the HTTP status or redirect alone. Check that ok is true in the final JSON. For example, if you receive {"ok":false,"error":"invalid_token"}, check that the submitted token matches the script property. For sheet_not_initialized, check the result of setup() and the sheet headings.

This example does not automatically POST the same measurement again if it receives no response: the sheet may already contain the data even if the response was lost. Every manual repetition of the same test adds a new row.

The STM32 project is platformio/google_sheets in the AC09 development repository. Open that folder in VS Code. The distribution URL is being prepared.

platformio/google_sheets/
├── platformio.ini
├── boards/ # AP03 board definition
├── variants/ # AP03 pin configuration
├── include/
│ └── config.example.h # Template for your local configuration
├── lib/
│ └── LeafonyAC09/ # AC09 initialization, AT communication, Cellular LPWA Cat-M1, TCP
├── src/ # Temperature/humidity, time, HTTPS, GAS transmission
├── docs/
│ └── library-evaluation.md # Public-library evaluation notes
└── gas/
└── Code.gs # The same GAS code shown above

AC09-specific operations are collected in the custom lib/LeafonyAC09 library. See the README.md in that directory for usage examples. The sample passes SIM settings such as the APN to the library. TLS uses ArduinoBearSSL, and HTTP responses are processed by a parser with size limits.

Temperature and humidity readings use SmartEverything HTS221 1.1.2, the same library as the temperature and humidity example. The code includes HTS221.h, initializes the sensor with smeHumidity.begin(), and reads values with readTemperature() and readHumidity(). platformio.ini pins the commit, so manual installation is not required.

The sensor measures continuously at 1 Hz, and the firmware obtains and sends new temperature and humidity readings every 10 minutes. After consuming old output, it waits up to 1500 ms for both new values, then checks their ranges. The library’s internal I²C reads include an indefinite wait, so certain communication failures during a read can stop processing. The 1500 ms limit is not a timeout for the entire library call.

Copy include/config.example.h to include/config.local.h and set the following:

SettingValue
GAS_URLThe GAS deployment URL, in the form https://script.google.com/macros/s/.../exec
GAS_API_TOKENThe same string as the GAS script property API_TOKEN
SIM_PROVIDER1 for 1NCE; 2 for SORACOM plan-D
ONE_NCE_APNThe APN displayed in the 1NCE portal. The template uses sensor.net; change it for SIMs assigned iot.1nce.net
SEND_INTERVAL_MS600000UL (10 minutes), the interval between starting readings and transmissions. If changing it, use at least 30 seconds

For SORACOM, the APN is soracom.io, authentication is PAP, and both username and password are sora. For 1NCE, the example uses PAP with an empty username and password. Check each operator’s APN instructions: 1NCE APN information and SORACOM APN settings (Japanese).

config.local.h and the built firmware contain the token. Keep them in your own environment and do not add them to public repositories. The project’s .gitignore excludes config.local.h.

  1. Initialize AC09 and connect to the Cellular LPWA Cat-M1 network.
  2. Initialize the HTS221 on 4-Sensors. To check the validity period of TLS certificates, retry obtaining the current time from the modem every 30 seconds until it is available.
  3. Read new temperature and humidity values with the library, encode the token, temperature_c, and humidity_pct as JSON, and send an HTTPS POST to GAS.
  4. GET the redirect destination returned by GAS and check for both HTTP 200 and {"ok":true}. This GET does not include the token or measurements.
  5. Repeat on a 10-minute cycle. Skip transmission for a cycle if temperature or humidity cannot be read or is out of range. Do not resend a measurement with an unknown transmission result; take a new reading on the next cycle.
  1. Set upload_port and upload_command in platformio.ini to your computer’s port and STM32CubeProgrammer installation path. The included command is a macOS example. See PlatformIO environment setup for OS-specific settings.

  2. Build in the project folder:

    Terminal window
    pio run -e leafony_ap03
  3. Set the STM32 leaf to Program, reset it, and upload:

    Terminal window
    pio run -e leafony_ap03 -t upload
  4. Return the switch to Run and reset. To check logs over USB, open the serial monitor:

    Terminal window
    pio device monitor -e leafony_ap03 -b 115200
  5. For battery operation, turn off the power and connect the battery to AV06. Disconnect USB, check that the STM32 switch is at Run, and turn on AV06. The firmware starts and proceeds with Cellular LPWA Cat-M1 connection and time acquisition without opening a serial monitor.

After startup and connection to the Cellular LPWA Cat-M1 network, the firmware reads the modem time with AT+CCLK?. If the time is unset or invalid, or acquisition fails, it waits 30 seconds after response processing finishes and checks again. There is no time limit for this wait. Until the time is obtained, it does not connect over HTTPS or send temperature and humidity. Transmission starts automatically in the cycle that obtains a valid time.

When checking logs over USB, the following appears while waiting. No action is required.

Waiting for network UTC; retrying in 30 seconds

Once a valid date and time are obtained, the following appears and transmission begins:

UTC clock set from modem

The network connection stays active while only time acquisition is failing. If an AT command response times out and communication status becomes unknown, the firmware waits 30 seconds, then retries from modem initialization and Cellular LPWA Cat-M1 connection. If network registration itself is rejected, it follows the normal reconnection procedure.

The acquired clock advances while the STM32 is running. After a power cycle, time acquisition starts again automatically. A USB connection or serial TIME input is not required.

This method requires the network and modem configuration to return the correct date and time through AT+CCLK?. Waiting alone will not start transmission in an environment that does not update the time. Automatic time acquisition with AC09 and the selected SIM has not yet been tested on hardware. Before battery operation, check that the time is acquired automatically after power-on. The example does not bypass certificate verification or transmit using a fixed date.

The MCU and modem remain active during the wait. Sleep-based power saving and battery life have not been evaluated.

The firmware prints Saved to Google Sheets in a cycle where saving is confirmed. The following illustrates the output format; it is not a log captured from hardware.

Temperature (C): 25.50
Humidity (%RH): 50.00
Saved to Google Sheets

If measurements appear without the saving confirmation, check the subsequent error messages.

Open the temperature sheet and check that three columns of data are appended. Column A is the GAS reception time (UTC), B is temperature (°C), and C is humidity (%RH). Check the rows from the computer test and from Leafony. Compare consecutive reception timestamps to verify that data is normally recorded approximately every 10 minutes.

Message or situationWhat to check
Configure include/config.local.hCheck that GAS_URL and GAS_API_TOKEN no longer contain template values
Network unavailableCheck the SIM status, APN, antenna, and power. After registration is rejected, leave at least five minutes between reconnection attempts
[AT] timeout; hardware restart requiredCheck the preceding timeout details and [diagnostic]. An AT timeout during network connection triggers modem reinitialization after 30 seconds
HTS221 initialization failed / HTS221 read failedCheck the 4-Sensors connection and I2C communication
Waiting for network UTCThe firmware retries automatically every 30 seconds. If waiting continues, check network/modem time synchronization settings and the AT+CCLK? response
Unexpected redirectCheck that GAS access is set to Anyone, without requiring a Google sign-in
GAS did not confirm savingTest the same URL and token from a computer and inspect the JSON error. Update the deployment after changing the code
HTTPS TLS connection failed; HTTP request was not sentCheck the failure reason, error number, and elapsed time in the preceding [TLS] log. The HTTP body for this request has not been sent
Google Sheets request failed / Result unavailableCheck the preceding [TLS] and [HTTPS] logs and whether a computer can write using the same GAS settings. The sheet may already contain the measurement even if the response was not received, so do not resend the same measurement

If processing stops while waiting for AT+CEREG?, it is still registering with the Cellular LPWA Cat-M1 network and has not started sending to Google. AT responses can be awaited for up to 120 seconds. The value printed after Network unavailable; retry delay (seconds) is the delay until the next connection attempt, not the response timeout.

On timeout, [AT] timeout details reports information such as received byte count and the length of an incomplete line, while [diagnostic] reports UART and AC09 signal states. STM32 reset flags are also printed at startup. BOR=1 can appear during normal power-on, so it alone does not prove a voltage drop during operation. Review the log from startup to timeout together with whether a manual reset was performed and how USB-C and the AV06 battery were connected.

After Cellular LPWA Cat-M1 connection and time acquisition, check [TLS] for the TLS connection result. reason=timeout indicates the processing deadline; reason=tls_error indicates a BearSSL error. [HTTPS] Request sent; waiting for HTTP response means the HTTP request has been written, but saving is not yet confirmed. The following response log shows the HTTP status, received byte count, and whether the response completed. Logs do not print the URL path, token, or HTTP body. Confirm successful saving with Saved to Google Sheets.