玩转Arduino(4)-天气预报

硬件 ESP32 开发板 0.96寸OLED 128*64 思路 ESP32 开发板自带WIFI和蓝牙 Step1 使用开发板连接家里的WIFI热点 注意修改代码中的WIFI SSID和password Step2 使用和风天气API,获取天气信息 和风天气的API默认访问https://devapi.qweather.com 需要使用SSL,对单片机开发非常麻烦,我重新封装代理了对应接口,简化了调用过程。 可使用 http://qweather.vearne.com/v7/weather/3d?location=101010100&lang=en&key=xxx 访问和风天气API,注意修改秘钥key 代码库为vearne/simpleQweather Step3 解析Response,将天气信息显示在OLED显示屏幕上 代码 完整代码 weather #include <WiFi.h> #include <HTTPClient.h> #include <Arduino.h> #include <Arduino_JSON.h> #include <SPI.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include "BitmapData.h" #define LOGO_HEIGHT 16 #define LOGO_WIDTH 16 #define WEATHER_X 50 const char *ssid = "vearne-Guest"; // Change this to your WiFi SSID const char *password = "xxxx"; // Change this to your WiFi password // Change key to your api key // https://dev.qweather.com/docs/configuration/project-and-key/ const char *url = "http://qweather.vearne.com/v7/weather/3d?location=101010100&lang=en&key=xxx"; #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels // Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) #define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); void setup() { Serial.begin(115200); while (!Serial) { delay(100); } // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(F("SSD1306 allocation failed")); for (;;) ; // Don't proceed, loop forever } // We start by connecting to a WiFi network Serial.println(); Serial.println("******************************************************"); Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); } void loop() { if (WiFi.status() == WL_CONNECTED) { HTTPClient http; // 开始请求 // http.addHeader(const String &name, const String &value) http.begin(url); // 发送 GET 请求 int httpCode = http.GET(); // 如果请求成功 if (httpCode > 0) { // 读取响应 String payload = http.getString(); Serial.println("响应内容:"); Serial.println(payload); // 结束请求 http.end(); JSONVar weatherData = JSON.parse(payload); if (JSON.typeof(weatherData) == "undefined") { Serial.println("Parsing input failed!"); return; } // 获取第一天的天气信息 JSONVar firstDay = weatherData["daily"][0]; String tempMax = (const char *)firstDay["tempMax"]; String tempMin = (const char *)firstDay["tempMin"]; String windScale = (const char *)firstDay["windScaleDay"]; String fxDate = (const char *)firstDay["fxDate"]; String iconDay = (const char *)firstDay["iconDay"]; switch (iconDay.toInt()) { case 100: drawOLED(sun, fxDate, tempMin, tempMax, windScale); break; case 101: drawOLED(cloudy, fxDate, tempMin, tempMax, windScale); break; case 102: drawOLED(cloud, fxDate, tempMin, tempMax, windScale); default: drawOLED(rain, fxDate, tempMin, tempMax, windScale); break; } } else { Serial.println("WiFi 未连接"); } delay(1000 * 300); // 等待 5min } } void drawOLED(const uint8_t *bitmap, String dateStr, String tempMin, String tempMax, String windScale) { display.clearDisplay(); // weather icon display.drawBitmap( 10, 10, bitmap, LOGO_WIDTH, LOGO_HEIGHT, 1); // temp & wind display.setTextSize(1); // Normal 1:1 pixel scale display.setTextColor(WHITE); // Draw white text display.setCursor(WEATHER_X, 10); display.print("temp: "); String s = ""; s.concat(tempMin); s.concat("~"); s.concat(tempMax); display.println(s); display.setCursor(WEATHER_X, 25); display.print("wind: "); display.println(windScale); // line display.drawLine(0, 45, display.width() - 1, 45, WHITE); // date display.setTextSize(2); // Normal 1:1 pixel scale display.setTextColor(WHITE); // Draw white text display.setCursor(5, 50); // Start at top-left corner display.println(dateStr); display.display(); delay(1000); } 库依赖 1.Adafruit_SSD1306 2.Arduino_JSON 3.ArduinoHttpClient 4.WiFi ...

November 23, 2024 · 3 min

试玩单片机的进展和感想

警告:本文仅用于萌叔自己总结之用,对其它人而言可能毫无营养,没有阅读价值。 1.总结 自2024年10月接触Arduino Uno电子平台以来, 萌叔购买了2块开发板和一系列的配件,尝试使用这些配件,并完成了若干小实验。具体如下 1.玩转Arduino(1)-自制温度湿度计 2.玩转Arduino(2)-按钮控制小灯 3.玩转Arduino(3)-感应式垃圾桶 4.玩转Arduino(4)-天气预报 2. 感想 其实感想才是萌叔想重点说的 2.1 单片机的自动化控制相对软件开发要简单很多 生活中大部分的单片机使用的场景都非常简单,比如停车场的自动升降杆、感应式垃圾桶。对应的代码开发量非常的少。相对而言,现在的软件开发非常的复杂,一个普通工程师需要掌握多种开发语言,好几种框架云原生等等卷的飞起。 2.2 整个单片机开发过程,犹如搭建积木 每种配件厂商几乎都有完善且对应的驱动包,特别典型的是步进电机。每种配件相当于一块积木,普通开发者通常只需要使用配件对应的驱动,就可以构建出非常炫酷的功能。最终达到的效果是1 + 1 > 2 的。 2.3 配件和开发板之间也有标准协议进行通讯 类似 I2C和 SPI。 2.4 配件底层涉及多学科知识 笔者在2.1 提到的单片机的自动化控制相对软件开发简单,指的是不涉及配件底层知识,单纯调用驱动的接口的情况。 一旦涉及底层,情况就复杂的多。 比如 RF433MHZ 涉及电磁波和电谐振 步进电机涉及电和磁力的转换 2.5 单片机的配套硬件和软件情况 单片机的硬件非常的弱,比如Arduino UNO的MCU ATmega328 运行内存只有2 KB。 以至于很多库(类似JSON和HTTP库)必须重新定制 软件方面是发现了FreeRTOS, FreeRTOS是有进程(Task)调度的。 单片机的场景,硬件性能弱,对应的软件也就简单,反而觉得更适合初学者学习了,萌叔推荐本科教育应该从单片机入手。 2.6 行业壁垒相比过去大幅降低了 这次尝试玩硬件,纯粹是一时兴起,然后萌叔发现上手几乎毫无难度。 这当然要感谢十几年的软件开发经历,但更多的还是要感谢时代的进步。 2.6.1 工业克鲁苏 中国的硬件制造商把开发板的价格卷到只要几十块,配件的价格卷到只有几块。 以前配件和开发板相连,往往还需要电烙铁焊接,现在一般都有对应扩展板,直接插端子就行。 2.6.2 Arduino平台 类似Arduino这种开源平台,使得开发板和配件驱动安装变得非常简单,代码开发调试也非常方便 2.6.1 开源精神 互联网(包含github)提供了完整的驱动和教程 2.6.2 AI 编程中遇到的很多问题多亏了chatGPT,不然我的开发速度要慢很多 3.开发板 ESP32 Arduino Uno 4.配件 编号 硬件设备 是否使用 备注 1 湿度温度传感器 是 2 LCD1602 是 3 按钮 是 4 LED小灯 是 5 舵机 是 6 单路马达 是 7 红外线接收器 是 8 超声波传感器 是 9 实时时钟模块 是 10 红外线传感器 是 11 RFID射频感应模块 否 12 步进电机 是 只能朝一个方向转? 13 OLED显示屏 是 14 旋转电位器 是 范围是0 ~ 978 15 继电器 是 收到高电平后,NO与COM形成通路 16 声音传感器 否 17 小喇叭 否 18 RF433MHZ 否 作者: vearne 文章标题: 试玩单片机的进展和感想 发表时间: 2024年11月22日 文章链接: https://vearne.cc/archives/40207 版权说明: CC BY-NC-ND 4.0 DEED ...

November 22, 2024 · 1 min

Content-Type请求示例

Content-Type请求示例 1.application/json 请求 python代码示例 import requests import json data = { 'a': 123, 'b': 456 } ## headers中添加上content-type这个参数,指定为json格式 headers = {'Content-Type': 'application/json'} ## post的时候,将data字典形式的参数用json包转换成json格式。 response = requests.post(url='http://127.0.0.1:9000/api/ttt', headers=headers, data=json.dumps(data)) 实际发出的请求体 POST /api/ttt HTTP/1.1 Host: 127.0.0.1:9000 User-Agent: python-requests/2.31.0 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive Content-Type: application/json Content-Length: 20 {"a": 123, "b": 456} 2. application/x-www-form-urlencoded 请求 python代码示例 import requests data = { 'a': 123, 'b': 456 } ## headers中添加上content-type这个参数,指定为x-www-form-urlencoded格式 headers = {'Content-Type': 'application/x-www-form-urlencoded'} ## post的时候,默认按x-www-form-urlencoded格式处理 response = requests.post(url='http://127.0.0.1:9000/api/ttt', headers=headers, data=data) 实际发出的请求体 POST /api/ttt HTTP/1.1 Host: 127.0.0.1:9000 User-Agent: python-requests/2.31.0 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive Content-Type: application/x-www-form-urlencoded Content-Length: 11 a=123&b=456

November 12, 2024 · 1 min

玩转Arduino(3)-感应式垃圾桶

思路 arduino接红外线传感器, 舵机、灯,红外线传感器被触发后,红灯亮起,舵机转动一定的角度,垃圾桶盖被打开,等待3秒后,舵机转回初始位置,垃圾桶盖关闭。 代码 #include <Servo.h> int infraredPin = 2; int lightPin = 6; int steerPin = 9; int closeAngle = 0; int openAngle = 80; Servo servo; void setup() { pinMode(infraredPin, INPUT); pinMode(steerPin, OUTPUT); pinMode(lightPin, OUTPUT); servo.attach(steerPin); // 初始状态: 垃圾桶盖子是关闭的 servo.write(closeAngle); } void loop() { if (digitalRead(infraredPin) == 0) { //检测到障碍物,输出低电平 digitalWrite(lightPin, HIGH); //亮灯 // 垃圾桶开盖 servo.write(openAngle); delay(3000); // 等待3秒 // clear digitalWrite(lightPin, LOW); // 灭灯 servo.write(closeAngle); } delay(200); } 演示视频 B站 ...

November 1, 2024 · 1 min

玩转Arduino(2)-按钮控制小灯

思路 按钮按下输出为低电平,按钮抬起时为高电平。 萌叔希望达到的效果是按一下按钮,小灯亮起,再按一次,小灯熄灭 需要捕获电位从 低电位 -> 高电位 的这个变化 代码 #define legPinIn 10 #define legPinOut 5 bool isLightOn = false; int preMode = HIGH; int currMode = HIGH; void setup() { Serial.begin(9600); pinMode(legPinIn, INPUT); pinMode(legPinOut, OUTPUT); } void loop() { isLightOn = getLightState(); if (isLightOn) { digitalWrite(legPinOut, HIGH); } else { digitalWrite(legPinOut, LOW); } delay(100); } bool getLightState() { if (digitalRead(legPinIn) != currMode) { preMode = currMode; currMode = digitalRead(legPinIn); if (preMode == LOW && currMode == HIGH) { isLightOn = !isLightOn; Serial.print("isLightOn: "); Serial.println(isLightOn); } } return isLightOn; } 演示视频 B站 ...

October 20, 2024 · 1 min

玩转Arduino(1)-自制温度湿度计

思路 从温湿度传感器读取温度和湿度信息,然后在LCD显示器上展示 代码 #include <DHT11.h> #include <Bonezegei_LCD1602_I2C.h> DHT11 dht11(2); Bonezegei_LCD1602_I2C lcd(0x27); void setup() { // Initialize serial communication to allow debugging and data readout. // Using a baud rate of 9600 bps. Serial.begin(9600); lcd.begin(); } void loop() { int temperature = 0; int humidity = 0; // Attempt to read the temperature and humidity values from the DHT11 sensor. int result = dht11.readTemperatureHumidity(temperature, humidity); // Check the results of the readings. // If the reading is successful, print the temperature and humidity values. // If there are errors, print the appropriate error messages. if (result == 0) { Serial.print("Temperature: "); Serial.print(temperature); Serial.print(" °C\tHumidity: "); Serial.print(humidity); Serial.println(" %"); String str = String("Temperat: ") + temperature + String(" C"); lcd.setPosition(0, 0); lcd.print(str.c_str()); lcd.setPosition(0, 1); str = str = String("Humidity: ") + humidity + String(" %"); lcd.print(str.c_str()); } else { // Print error message based on the error code. Serial.println(DHT11::getErrorString(result)); } delay(10000); } 参考资料 1.DHT11驱动 2.LCD1602驱动 ...

October 20, 2024 · 1 min

玩转高性能日志库ZAP (7)-标识

1.前言 在Java中我们常常会这样使用日志 import org.slf4j.Logger; import org.slf4j.LoggerFactory; class Main { final static Logger log = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { log.info("info, hello {}", "slf4j"); log.error("error"); } } 输出 6:30:03.350 [main] INFO Main - info, hello slf4j 16:30:03.356 [main] ERROR Main - error 这里在每条日志中都有类名,可以很容易从文件中过滤出这个类相关的日志。 2.zap中使用name标识 zap中的玩法 package main import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "time" ) func main() { config := zap.NewProductionConfig() config.Encoding = "console" config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05") logger, _ := config.Build() defer logger.Sync() // 输出该条日志在代码文件中的行号 logger = logger.WithOptions(zap.AddCaller()) logger.Info("failed to fetch URL", zap.Int("attempt", 3), zap.Duration("backoff", time.Second), ) go func() { // worker1 // 携带name标识 logger1 := logger.Named("worker1") for { time.Sleep(time.Millisecond * 200) logger1.Info("hello1") } }() go func() { // worker2 logger2 := logger.Named("worker2") for { time.Sleep(time.Millisecond * 200) logger2.Info("hello2") } }() time.Sleep(3 * time.Second) } 输出 ...

October 9, 2024 · 2 min

一次Redis内存排查记录

1. 起因 萌叔手上有一个测试用的Redis集群,集群是Master-slave模式, 平常也不怎么使用。 于是打算清理释放一下内存,打算降低一下配置。 使用FLUSHALL FLUSHALL 可以看到所有的key都已经被清空 > info Keyspace # Keyspace 查看内存开销情况 > info memory # Memory used_memory:537921192 used_memory_human:513.00M used_memory_rss:544067584 used_memory_rss_human:518.86M used_memory_peak:650406864 实例的内存开销超过500MB,内存让狗吃了? 2. 排查 # Memory used_memory:537921192 used_memory_human:513.00M used_memory_rss:544067584 used_memory_rss_human:518.86M used_memory_peak:650406864 used_memory_peak_human:620.28M used_memory_peak_perc:82.71% used_memory_overhead:537863648 used_memory_startup:791600 used_memory_dataset:57544 used_memory_dataset_perc:0.01% allocator_allocated:537984728 allocator_active:538705920 allocator_resident:548462592 total_system_memory:201219264512 total_system_memory_human:187.40G used_memory_lua:40960 used_memory_lua_human:40.00K used_memory_scripts:216 used_memory_scripts_human:216B number_of_cached_scripts:1 maxmemory:1073741824 maxmemory_human:1.00G maxmemory_policy:allkeys-lru allocator_frag_ratio:1.00 allocator_frag_bytes:721192 allocator_rss_ratio:1.02 allocator_rss_bytes:9756672 rss_overhead_ratio:0.99 rss_overhead_bytes:-4395008 mem_fragmentation_ratio:1.01 mem_fragmentation_bytes:6229608 mem_not_counted_for_evict:0 mem_replication_backlog:536870912 // 约为512MB mem_clients_slaves:16922 mem_clients_normal:183998 mem_aof_buffer:0 mem_allocator:jemalloc-5.1.0 active_defrag_running:0 lazyfree_pending_objects:0 # Replication role:master connected_slaves:1 slave0:ip=192.168.88.29,port=9981,state=online,offset=23482674189,lag=0 master_replid:b58677283938996fdae00f7692d24d11d88ff488 master_replid2:effdd3cd4ab2030a2d49273e0cf2ed74df4bccb9 master_repl_offset:23482674347 second_repl_offset:17046600529 repl_backlog_active:1 repl_backlog_size:536870912 // 约为512MB repl_backlog_first_byte_offset:22945803436 repl_backlog_histlen:536870912 # Cluster cluster_enabled:0 # Keyspace 将INFO命令返回的所有内容都交给chatGPT分析,笔者找到了答案,主要原因是复制积压缓冲区设置的过大。 ...

September 11, 2024 · 1 min

RocketMQ架构设计中的”暴力美学”(2)-故障处理

本文基于rocketmq-all-4.8.0 1.引言 人们在潜意识里,总会觉得复杂且精巧的东西是好东西。但是这个复杂这个词在软件架构设计中,却不一定是好事情。 因为过于精巧和复杂的系统往往意味着系统更难以维护,出现问题后,故障更难排查。 萌叔在阅读和分享RocketMQ的过程中, 发现它有很多设计非常的简单粗暴,堪称”暴力美学”的典范, 同时又给人眼前一亮的感觉(还能这么玩)。 2.故障处理 在介绍故障处理机制时,我们假定一个场景 2.1 架构 假定broker有2组 group1 Master1和Slave1 group2 Master2和Slave2 2.2 故障发生 故障发生前,萌叔创建了1个topic,指定分片数为4 mqadmin updateTopic -n <namesrvAddr> -c <clusterName> -t myTopic -q 4 这里需要强调一下,这里指定的4个分片,并不是全局4个分片,而是每个broker有4个分片,情况如下图。 此时 Topic路由信息形如: { "OrderTopicConf": "", "queueDatas": [{ "brokerName": "group1", "readQueueNums": 4, "writeQueueNums": 4, "perm": 6, // 可读可写 "topicSynFlag": 0 }, { "brokerName": "group2", "readQueueNums": 4, "writeQueueNums": 4, "perm": 6, // 可读可写 "topicSynFlag": 0 }], "brokerDatas": [{ "cluster": "Default_Cluster", "brokerName": "group1", "brokerAddrs": { "0": "192.168.12.123:10911", // master "1": "192.168.12.127:10911" // slave } }, { "cluster": "Default_Cluster", "brokerName": "group2", "brokerAddrs": { "0": "192.168.12.220:10911", "1": "192.168.12.12:10911" } }] } 生产者消息路由策略:Round Robin 消费者消息路由策略:AllocateByAveragely ...

August 7, 2024 · 2 min

heavykeeper算法的一些设计思想

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引言 topk问题是十分经典的算法问题,它的核心是从大量数据中找出前k个最大或者最小的元素。 它对于提高缓存的命中率,以及发现新增热点的发现有很大的帮助。 对于topk问题而言, 如果庞大的数据集本身是固定的,萌叔觉得topk问题本身还不算复杂, 顶多算是一个大数据计算的问题。 但如果这个庞大的数据集本身就处于变化中,不断在新增元素, topk问题就会变得非常复杂。 2023年萌叔看到一篇B站的文章,介绍了B站使用的heavykeeper算法来计算topk问题,取得了比较好的效果。 本文不是纯粹意义上的介绍heavykeeper算法的,萌叔想探讨一下里面的设计理念。 heavykeeper 对应的参考实现见参考资料4 B站给出的试验结果数据 2.数据结构 heavykeeper的设计目标是解决高流速情况下的topk问题且topk的元素出现的频率必须显著高于其它元素。 互联网数据大体满足二八定律,是符合要求的。 1)heavykeeper使用的d行w列的二维数组, 2)流体中的每个元素在 每一行上都有一个对应的位置,由哈希函数决定,哈希函数从h1()、h2() 到 hd() 这个数据结构与Count-Min Sketch算法是非常像的,唯一的区别是heavykeeper中二维数组的bucket,是一个结构体 type bucket struct { fingerprint uint64 // 比Count-Min Sketch算法多出来的字段 count uint64 } 3.处理逻辑 对应代码 heavy_keeper.go func (topk *TopK) jobAdder() { for item := range topk.items { itemFingerprint := xxhash.Checksum64S(item, uint64(topk.seed)) var maxCount uint64 itemHeapIdx, itemHeapExist := topk.minHeap.Find(item) heapMin := topk.minHeap.Min() // compute d hashes for i := uint32(0); i < topk.depth; i++ { bI := make([]byte, 4) binary.LittleEndian.PutUint32(bI, uint32(i)) bucketNumber := xxhash.Checksum64S(append([]byte(item), bI...), uint64(topk.seed)) % uint64(topk.width) topk.buckets[i][bucketNumber].mu.Lock() fingerprint := topk.buckets[i][bucketNumber].fingerprint count := topk.buckets[i][bucketNumber].count if count == 0 { // Case 1 topk.buckets[i][bucketNumber].fingerprint = itemFingerprint topk.buckets[i][bucketNumber].count = 1 maxCount = max(maxCount, 1) } else if fingerprint == itemFingerprint { // Case 2 if itemHeapExist || count <= heapMin { topk.buckets[i][bucketNumber].count++ maxCount = max(maxCount, topk.buckets[i][bucketNumber].count) } } else { // Case 3 decay := math.Pow(topk.decay, float64(count)) if rand.Float64() < decay { topk.buckets[i][bucketNumber].count-- if topk.buckets[i][bucketNumber].count == 0 { topk.buckets[i][bucketNumber].fingerprint = itemFingerprint topk.buckets[i][bucketNumber].count = 1 maxCount = max(maxCount, 1) } } } topk.buckets[i][bucketNumber].mu.Unlock() } // update heap if itemHeapExist { topk.minHeap.Fix(itemHeapIdx, maxCount) } else { topk.minHeap.Add(minheap.Node{ Count: maxCount, Item: item, }) } } } 3.1 众数问题 看到上面的处理逻辑,很多读者可能会感到懵,为什么这样做就可以找出topk的元素? 为了解释heavykeeper的设计理念,我们先来看一个求众数问题 LeetCode 第169题 ...

July 25, 2024 · 2 min