

The biggest hurdle of creating an App for android device is learning how to use its tool chain.
This is how I did it using AI and AI Agent or AI Assistance.
Continue reading


The biggest hurdle of creating an App for android device is learning how to use its tool chain.
This is how I did it using AI and AI Agent or AI Assistance.
Continue readingESP32-Wrover does have SPI or SDMMC, that will require additional purchase of modules for the microSD or other SD physical card module.
The need to create proper data acquisition (DAQ) using ESP32-Wrover will be easier by leveraging free service offered by ThingSpeak from Mathlab.
From the previous project https://chow.karmeng.my/2025/03/28/programming-esp32-wrover-with-dht22-am2302-humidity-and-temperature-sensor/ , I am intent to make use of the light sensor. L1B01 light module to collect data of ambience light.

Referring to the original material at ESP32 with DHT11/DHT22 Temperature and Humidity Sensor using Arduino IDE | Random Nerd Tutorials
The reference is straight forward; however, it is not for the esp32-wrover.
Based on the from the reference, using educated guess without reading the specification, it is a hindsight that the original wiring will not work.

It didn’t work, was getting the error “Failed to read from DHT sensor!” from the sample code.
DHTPIN were used is 4, which I had wrongly assumed it to be the physical pin number. Little did I know that, the reference is referring to GPIO4 instead of the physical pin number.
Based on the reference, ESP32-wrover spec sheet page 9, the physical pin 4 is the SENSOR_VP.
Continue readingThis post is based mostly on Installing ESP32 Board in Arduino IDE 2 (Windows, Mac OS X, Linux) | Random Nerd Tutorials it is a good resource to get started on verify the ESP32-WROVER is working.
The hardest part is to determine what are the connector chip and installing libraries into Arduino IDE. Refer to the shared link in the beginning of this post.
Code from DeepSeek for a complicated hello world, assuming to make the LED blinks in morse code:
#include <Arduino.h>
#define LED_PIN 2 // Built-in LED on GPIO 2
// Morse code representations for A-Z
const char* morseCodes[] = {
".-", // A
"-...", // B
"-.-.", // C
"-..", // D
".", // E
"..-.", // F
"--.", // G
"....", // H
"..", // I
".---", // J
"-.-", // K
".-..", // L
"--", // M
"-.", // N
"---", // O
".--.", // P
"--.-", // Q
".-.", // R
"...", // S
"-", // T
"..-", // U
"...-", // V
".--", // W
"-..-", // X
"-.--", // Y
"--.." // Z
};
void setup() {
pinMode(LED_PIN, OUTPUT); // Set the LED pin as an output
Serial.begin(115200);
}
void loop() {
String message = "SOS"; // Message to transmit in Morse code
message.toUpperCase(); // Convert message to uppercase
// Transmit the message in Morse code
for (int i = 0; i < message.length(); i++) {
char currentChar = message[i];
if (currentChar >= 'A' && currentChar <= 'Z') {
transmitMorse(morseCodes[currentChar - 'A']); // Transmit Morse code for the character
} else if (currentChar == ' ') {
delay(1400); // Gap between words (7 units)
}
delay(600); // Gap between letters (3 units)
}
delay(2000); // Wait before repeating the message
}
// Function to transmit a Morse code pattern
void transmitMorse(const char* morseCode) {
for (int i = 0; i < strlen(morseCode); i++) {
if (morseCode[i] == '.') {
blinkDot(); // Transmit a dot
} else if (morseCode[i] == '-') {
blinkDash(); // Transmit a dash
}
delay(200); // Gap between dots/dashes (1 unit)
}
}
// Function to blink a dot (short flash)
void blinkDot() {
digitalWrite(LED_PIN, HIGH); // Turn the LED on
delay(200); // Dot duration (1 unit)
digitalWrite(LED_PIN, LOW); // Turn the LED off
}
// Function to blink a dash (long flash)
void blinkDash() {
digitalWrite(LED_PIN, HIGH); // Turn the LED on
delay(600); // Dash duration (3 units)
digitalWrite(LED_PIN, LOW); // Turn the LED off
}
Code to connect to WiFi:
#include <WiFi.h>
// Replace with your network credentials
const char* ssid = "myhome4iot";
const char* password = "i have the longest wifi password ever";
void setup() {
Serial.begin(115200);
// Connect to Wi-Fi
WiFi.begin(ssid, password);
Serial.println("Connecting to Wi-Fi...");
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
// Connection successful
Serial.println("\nWi-Fi connected!");
// Get and print network information
IPAddress ip = WiFi.localIP();
IPAddress gateway = WiFi.gatewayIP();
IPAddress dns = WiFi.dnsIP();
Serial.println("Network Information:");
Serial.print("IP Address: ");
Serial.println(ip);
Serial.print("Gateway: ");
Serial.println(gateway);
Serial.print("DNS Server: ");
Serial.println(dns);
}
void loop() {
// Nothing to do here
}

Code to scan WiFi:
#include <WiFi.h>
void setup() {
Serial.begin(115200);
// Set ESP32 to station mode
WiFi.mode(WIFI_STA);
WiFi.disconnect(); // Disconnect from any previous connection
delay(100);
Serial.println("Starting Wi-Fi scan...");
}
void loop() {
// Scan for nearby Wi-Fi networks
int numNetworks = WiFi.scanNetworks();
if (numNetworks == 0) {
Serial.println("No networks found.");
} else {
Serial.print(numNetworks);
Serial.println(" networks found:");
for (int i = 0; i < numNetworks; i++) {
// Print SSID and RSSI for each network
Serial.print(i + 1);
Serial.print(": ");
Serial.print(WiFi.SSID(i)); // SSID
Serial.print(" (");
Serial.print(WiFi.RSSI(i)); // Signal strength (RSSI)
Serial.print(" dBm)");
Serial.print(" [");
Serial.print(getEncryptionType(WiFi.encryptionType(i))); // Encryption type
Serial.println("]");
}
}
Serial.println("-----------------------------");
delay(10000); // Wait 10 seconds before scanning again
}
// Function to convert encryption type to a human-readable string
String getEncryptionType(wifi_auth_mode_t encryptionType) {
switch (encryptionType) {
case WIFI_AUTH_OPEN:
return "Open";
case WIFI_AUTH_WEP:
return "WEP";
case WIFI_AUTH_WPA_PSK:
return "WPA";
case WIFI_AUTH_WPA2_PSK:
return "WPA2";
case WIFI_AUTH_WPA_WPA2_PSK:
return "WPA/WPA2";
case WIFI_AUTH_WPA2_ENTERPRISE:
return "WPA2 Enterprise";
case WIFI_AUTH_WPA3_PSK:
return "WPA3";
case WIFI_AUTH_WPA2_WPA3_PSK:
return "WPA2/WPA3";
default:
return "Unknown";
}
}
Unfortunately, due to the ESP32-WROVER hardware limitation, any modern 5GHz WiFi will not be able to be scanned or detected. On top of that, the stock ESP32-WROVER-IE needs to have a actual wifi cable to extend its range.


For the folks that are impatient, the POC of DeepSeek is here KarMeng / deepseek-simple / localdev — Bitbucket
Continue readingThis post is based on reference to Use instance metadata to manage your EC2 instance – Amazon Elastic Compute Cloud
Metadata is a powerful tool for AWS users. It allows users to make query of data describing EC2 instances, and making a self reference API call.
By default any linux AMI will have curl build in, hence using of metadata will be simplified.
One of the best test case is automating input into shell script that will requires a lot of user prompt is automating or at least make configuration of setting up openvpn using AWS Lightsail easier.
!#/bin/bash
sudo chmod 777 ./openvpn-install.sh
sudo ./openvpn-install.sh << INPUT
y
1
1
11
n
n
client
1
INPUT
sudo cp /root/client.ovpn /home/ubuntu
sudo chmod 777 /home/ubuntu/client.ovpn
Above shell script will cause the openvpn-install.sh to fail in AWS, as the script does not provide public and local ip of the instance.
Below script are including the AWS Metadata
!#/bin/bash
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
PUBLICIP=`curl -X GET "http://169.254.169.254/latest/meta-data/public-ipv4" -H "X-aws-ec2-metadata-token: $TOKEN"`
LOCALIP=`curl -X GET "http://169.254.169.254/latest/meta-data/local-ipv4" -H "X-aws-ec2-metadata-token: $TOKEN"`
sudo chmod 777 ./openvpn-install.sh
sudo ./openvpn-install.sh << INPUT
$LOCALIP
$PUBLICIP
y
1
1
11
n
n
client
1
INPUT
sudo cp /root/client.ovpn /home/ubuntu
sudo chmod 777 /home/ubuntu/client.ovpn
By adding the metadata the ovpn file will be populated with correct IP.
AWS Metadata allow automation to be made simpler by running scripts that requires self-reference metadata to configure newly booted up EC2 instance(s).
@echo off
powershell -version 2.0 -ExecutionPolicy unrestricted %~dp0generate_putty_sessions.ps1
regedit.exe /s %userprofile%\putty_list.reg
#Preloading scripts
#Removing old reg file
if ( Test-Path $env:userprofile\putty_list.reg){
del $env:userprofile\putty_list.reg
}
#Check environment for Windows x86 or x86_64
if ([IntPtr]::Size -eq 4){
if ( Test-Path "C:\Program Files\AWS Tools\PowerShell\AWSPowerShell"){
import-module "C:\Program Files\AWS Tools\PowerShell\AWSPowerShell\AWSPowerShell.psd1"
}
else{
write-host "AWS Tools for PowerShell was not install, exiting. Download at http://aws.amazon.com/powershell/"
exit
}
}
else{
if ( Test-Path "C:\Program Files (x86)\AWS Tools\PowerShell\AWSPowerShell"){
import-module "C:\Program Files (x86)\AWS Tools\PowerShell\AWSPowerShell\AWSPowerShell.psd1"
}
else{
write-host "AWS Tools for PowerShell was not install, exiting. Download at http://aws.amazon.com/powershell/"
exit
}
}
#Check env variable for required EC2 configuration
if (-not (Test-Path Env:\EC2_HOME)){
write-host "Environment variable EC2_HOME was not found, please ensure your EC2 API Tools were properly installed or configured or setup."
exit
}
if (-not (Test-Path Env:\EC2_CERT)){
write-host "Environment variable EC2_CERT was not found, please ensure your EC2 API Tools were properly installed or configured or setup."
exit
}
if (-not (Test-Path Env:\EC2_PRIVATE_KEY)){
write-host "Environment variable EC2_PRIVATE_KEY was not found, please ensure your EC2 API Tools were properly installed or configured or setup."
exit
}
#Get my script path
$myPath = split-path -parent $MyInvocation.MyCommand.Definition
if(-not (Test-Path -path $myPath\reg_header.txt)){
write-host "Please make sure reg_header.txt is in " $myPath
exit
}
if(-not (Test-Path -path $myPath\reg_putty.txt)){
write-host "Please make sure reg_header.txt is in " $myPath
exit
}
Copy-Item $myPath\reg_header.txt $env:userprofile
Copy-Item $myPath\reg_putty.txt $env:userprofile
#Main body and function of the script.
#Creating file to link instance ID with Public DNS
ec2-describe-instances --filter `"virtualization-type=paravirtual`" --filter `"instance-state-name=running`" --filter `"tag:Name=/*/*`" | Select-String -pattern INSTANCE -caseSensitive | foreach { "$($_.ToString().split()[1,3])" >> $env:userprofile\awsinstanceIP.tmp}
#Creating a file to link instance ID with Name tag
ec2-describe-instances --filter `"virtualization-type=paravirtual`" --filter `"instance-state-name=running`" --filter `"tag:Name=/*/*`" | Select-String -pattern Name -caseSensitive | foreach { "$($_.ToString().split()[2,4])" >> $env:userprofile\awsinstanceName.tmp}
# Clean up results, removing RenderWorkerGroup
Get-Content $env:userprofile\awsinstanceName.tmp | Select-String -pattern RenderWorkerGroup -NotMatch | foreach { "$($_.ToString().split()[0,1])" >> $env:userprofile\awsinstanceNameClean.tmp}
#$awsInstanceIDIP = Get-Content $env:userprofile\awsinstanceIP.tmp
$awsInstanceCleanName = Get-Content $env:userprofile\awsinstanceNameClean.tmp
$count = 0
# Create HashTable from File.
ForEach ($line in $awsInstanceCleanName) {
if ($count -le 0 ) {
$myHash = @{ $line.ToString().Split()[0] = $line.ToString().Split()[1]}
}
else{
$myHash.Set_Item($line.ToString().Split()[0], $line.ToString().Split()[1])
}
$count = $count + 1
}
$count = 0
Get-Content $env:userprofile\awsinstanceIP.tmp | ForEach-Object {
$line = $_
$myHash.GetEnumerator() | ForEach-Object {
if ($line -match $_.Key)
{
if ($_.value.ToString().Contains("render-worker")){
$replacement = $_.Key.ToString() + " " + $_.Value.ToString() + "/" + $_.Key.ToString()
}
else{
$replacement = $_.Key.ToString() + " " + $_.Value.ToString()
}
$line = $line -replace $_.Key, $replacement
}
}
$line
} | Set-Content -Path $env:userprofile\awsinstanceResult.tmp
del $env:userprofile\awsinstanceIP.tmp
del $env:userprofile\awsinstanceName.tmp
del $env:userprofile\awsinstanceNameClean.tmp
$awsinstanceResult = Get-Content $env:userprofile\awsinstanceResult.tmp
#Adding header into file content.
Add-Content $env:userprofile\awsinstanceReg_List.tmp $(Get-Content $env:userprofile\reg_header.txt)
Add-Content $env:userprofile\awsinstanceReg_List.tmp "`r"
# Populating body of the file before converting into registry file.
foreach ($line in $awsinstanceResult){
$reg_line = "`[HKEY_CURRENT_USER\Software\Simontatham\PuTTY\Sessions\" + $line.ToString().Split()[1] + "]"
Add-Content $env:userprofile\awsinstanceReg_List.tmp $reg_line
$reg_line = "`"HostName`"=`"" + $line.ToString().Split()[2] + "`""
Add-Content $env:userprofile\awsinstanceReg_List.tmp $reg_line
# Add fillers to the sessions
Add-Content $env:userprofile\awsinstanceReg_List.tmp $(Get-Content $env:userprofile\reg_putty.txt)
Add-Content $env:userprofile\awsinstanceReg_List.tmp "`r"
}
Get-Content $env:userprofile\awsinstanceReg_List.tmp | Add-Content $env:userprofile\putty_list.reg
#Removing all temporary files
del $env:userprofile\awsinstanceReg_List.tmp
del $env:userprofile\awsinstanceResult.tmp
del $env:userprofile\reg_header.txt
del $env:userprofile\reg_putty.txt
Cloudkick which is part of RackSpace provides Agent based monitoring for external servers and cloud servers. Other than monitoring computer resources such as CPU, memory, HDD utilization. It is able to cater for customed check which is known as customed plugin. In other words, Cloudkick offers limitless way of administrator to monitor their IT assets as host level which is limited to the operating systems shell scripting support as well as limited to the creativity of the scripters.
Continue readingOne of the challenges that I have faced in linux environment were to automate installation.
Of course it is simple to script using bash in linux to run rpm or deb binary installation.
What if the installation comes with an interactive installation or installer which is coded as a bash script. Yes, I am looking at you OSSEC .
Continue reading