2024年3月

本文介绍三种使用纯 CSS 实现星级评分的方式。每种都值得细品一番~

aa.gif

五角星取自 Element Plus 的 svg 资源

image.png

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" style="">
    <path
        fill="currentColor"
        d="M283.84 867.84 512 747.776l228.16 119.936a6.4 6.4 0 0 0 9.28-6.72l-43.52-254.08 184.512-179.904a6.4 6.4 0 0 0-3.52-10.88l-255.104-37.12L517.76 147.904a6.4 6.4 0 0 0-11.52 0L392.192 379.072l-255.104 37.12a6.4 6.4 0 0 0-3.52 10.88L318.08 606.976l-43.584 254.08a6.4 6.4 0 0 0 9.28 6.72z">
    </path>
</svg>

三种实现方式的 html 结构是一样的

<div>
  <input type="radio" name="radio" id="radio1">
  <label for="radio1">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" style=""><path fill="currentColor" d="M283.84 867.84 512 747.776l228.16 119.936a6.4 6.4 0 0 0 9.28-6.72l-43.52-254.08 184.512-179.904a6.4 6.4 0 0 0-3.52-10.88l-255.104-37.12L517.76 147.904a6.4 6.4 0 0 0-11.52 0L392.192 379.072l-255.104 37.12a6.4 6.4 0 0 0-3.52 10.88L318.08 606.976l-43.584 254.08a6.4 6.4 0 0 0 9.28 6.72z"></path></svg>
  </label>
  <input type="radio" name="radio" id="radio2">
  <label for="radio2">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" style=""><path fill="currentColor" d="M283.84 867.84 512 747.776l228.16 119.936a6.4 6.4 0 0 0 9.28-6.72l-43.52-254.08 184.512-179.904a6.4 6.4 0 0 0-3.52-10.88l-255.104-37.12L517.76 147.904a6.4 6.4 0 0 0-11.52 0L392.192 379.072l-255.104 37.12a6.4 6.4 0 0 0-3.52 10.88L318.08 606.976l-43.584 254.08a6.4 6.4 0 0 0 9.28 6.72z"></path></svg>
  </label>
  <input type="radio" name="radio" id="radio3">
  <label for="radio3">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" style=""><path fill="currentColor" d="M283.84 867.84 512 747.776l228.16 119.936a6.4 6.4 0 0 0 9.28-6.72l-43.52-254.08 184.512-179.904a6.4 6.4 0 0 0-3.52-10.88l-255.104-37.12L517.76 147.904a6.4 6.4 0 0 0-11.52 0L392.192 379.072l-255.104 37.12a6.4 6.4 0 0 0-3.52 10.88L318.08 606.976l-43.584 254.08a6.4 6.4 0 0 0 9.28 6.72z"></path></svg>
  </label>
  <input type="radio" name="radio" id="radio4">
  <label for="radio4">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" style=""><path fill="currentColor" d="M283.84 867.84 512 747.776l228.16 119.936a6.4 6.4 0 0 0 9.28-6.72l-43.52-254.08 184.512-179.904a6.4 6.4 0 0 0-3.52-10.88l-255.104-37.12L517.76 147.904a6.4 6.4 0 0 0-11.52 0L392.192 379.072l-255.104 37.12a6.4 6.4 0 0 0-3.52 10.88L318.08 606.976l-43.584 254.08a6.4 6.4 0 0 0 9.28 6.72z"></path></svg>
  </label>
  <input type="radio" name="radio" id="radio5">
  <label for="radio5">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" style=""><path fill="currentColor" d="M283.84 867.84 512 747.776l228.16 119.936a6.4 6.4 0 0 0 9.28-6.72l-43.52-254.08 184.512-179.904a6.4 6.4 0 0 0-3.52-10.88l-255.104-37.12L517.76 147.904a6.4 6.4 0 0 0-11.52 0L392.192 379.072l-255.104 37.12a6.4 6.4 0 0 0-3.52 10.88L318.08 606.976l-43.584 254.08a6.4 6.4 0 0 0 9.28 6.72z"></path></svg>
  </label>
</div>

利用
radio + label
的方式实现点击效果;将
label

for
属性保持和
radio

id
一致,并将
radio
框隐藏,这样点击
label
就是点击
radio
了;
label
在这里就是每个星星;

html,body{
  width:100%;height:100%;
}
body{
  display:flex;
  justify-content:center;
  align-items:center;
}
div{
  display : flex;
  justify-content:center;
  align-items:center;
}

div input{
  display:none;
}

div label{
  width:50px;height:50px;
  padding:0 4px;
  color:#ccc;
  cursor:pointer;
}

html 布局效果如下:

image.png

通常星级评分效果包括鼠标滑入和点击,滑入或点击到第几颗星的位置,该位置之前的星高亮,之后的星不高亮或者有高亮的则取消高亮;

接下来分别阐述三种 CSS 实现方式;

1、
:has()选择器
+
input:checked

当点击星星时,高亮当前星星

input:checked + label{
    color:gold;
}

input:checked + label
表示 选择紧挨着已选中 input 框的后面一个 label;

当鼠标移入星星时,高亮当前星星,并且该位置之后的星星取消高亮;

label:hover{
  cursor:pointer;
  color:gold;
  & ~ label{
    color:#ccc!important;
  }
}

那么如何让该位置之前的星星也高亮呢,目前的兄弟选择器包括 + 和 ~ ,但都不能选择之前的兄弟元素;此时
:has()
选择器就登场了;

:has()
提供了一种针对引用元素选择父元素或者先前的兄弟元素的方法。

比如:

a:has(p)
表示选择包含子元素 p 的 a 元素;

a:has(> p)
表示选择有直接后代 p 元素的 a 元素,也就是 p 只能是 a 的 "儿子" 元素;

a:has(+ p)
表示选择后面紧跟着的兄弟元素是 p 的 a 元素;

所以回到上面问题,当鼠标移入星星时,让该位置之前的所有星星也高亮,可以这么做

div:has(label:hover) label:not(:hover,:hover ~ *){
  color:gold;
}

label:not(:hover,:hover ~ *)
表示排除当前 hover 的 label 和之后的所有元素;也就自然选择了前面所有星星;

bb.gif

同样,当点击星星时,点亮当前选择的之前所有的星星也如此

div:has(input:checked) label:not(input:checked ~ label){
  color:gold;
}

div:has(input:checked)
表示选择包含被选中的 input 的 div;

label:not(input:checked ~ label)
表示排除当前选中的 input 后面的所有 label,也就选择到前面所有的 label 了;

完整示例

cc.gif

2、
:indeterminate
+
input:checked
巧妙实现

这种实现的思路是,假设初始所有的星星都是高亮的,鼠标移入或点击时保持前面星星的高亮,取消后面星星的高亮;

div label{
  width:50px;height:50px;
  padding:0 4px;
  color:gold;  => 默认星星高亮
  cursor:pointer;
}

image.png

然后当鼠标移入或点击时,取消该位置后面的星星的高亮

div input:checked ~ input + label,
div label:hover ~ label{
  color:#ccc;
}

gg.gif

但一开始默认设置的星星是高亮的,但页面上并不想在 radio 未被选中时高亮,这时
:indeterminate
就登场了;

:indeterminate
表示任意的状态不确定的表单元素。对于
radio
元素,
:indeterminate
表示当表单中具有相同名称值的所有单选按钮均未被选中时。

所以这里设置每个星星在对应的 radio 的未被选中时非高亮;
并且只是在初始状态,鼠标移入时这种初始状态就应该被改变

div:not(:hover) input:indeterminate + label,
div:not(:hover) input:checked ~ input + label,
div input:hover ~ input + label{
  color:#ccc;
}

:not()
表示用来匹配不符合一组选择器的元素;
div:not(:hover)
表示鼠标移入时,不匹配这行规则,这样在初始状态下或者在鼠标点击星星后,鼠标移入仍然会高亮当前点击位置之前的星星;

这样效果就达到了;

完整示例

cc.gif

3、
flex-direction:row-reverse;
+
input:checked
巧妙实现

目前 html 布局是从左到右布局,但如果我们倒过来呢,从右到左布局;

div{
  width:300px;
  display:flex;
  /* 从右往左排列 */
  flex-direction:row-reverse;
  justify-content:space-around;
}

dd.png

那么之前利用
:has()
选择之前的兄弟元素现在就可以直接用
~
来选择了;

// 之前
label:not(input:checked ~ label){
  color:gold;
}

// 现在
label:hover ~ label{
  color:gold;
}

dd.gif

点击星星也是

input:checked ~ label{
  color:gold;
}

但是这样还不够完善

ee.gif

当我们点击第二颗星星时,鼠标滑入到第三个星星,第二颗星星并没有取消高亮,所以这里还是得借助下
:has()

label:has(~ label:hover){
  color:#ccc;
}

上面表示选择后面被 hover 的兄弟元素的元素,也就是 hover 元素的前面的所有元素;这样就没问题了;

完整示例

ff.gif

总结

以上使用了三种纯 css 实现星级评分的方式;

  • :has()选择器
    +
    input:checked
  • :not()选择器
    +
    input:checked
  • flex-direction:row-reverse;
    +
    input:checked
    巧妙实现

特别是
:has()
选择器可以选择之前的兄弟元素,搭配
:not()
能发挥很多作用,以前很多需要用 js 实现的效果或许现在可以用
:has()
来试试了;

附上
:has()

:not()
的兼容性截图

:has()

:not()

各位看官们,如果对本文感兴趣,麻烦动动你们的发财手,点点赞~

一、分析页面

打开
虎嗅网
,点击【24小时】
在这里插入图片描述

本次采集,我们以这24小时的热门新闻为案例。

1.1、分析请求

F12打开开发者模式,然后点击Network后点击任意一个请求,Ctrl+F开启搜索,输入标题雷军回应 ,开始搜索
在这里插入图片描述
可以看到请求地址为https://www.huxiu.com/moment/ 但是返回的内容不是json格式,而是html源码,结合上次博客园采集经验我们需要解析html源码来获取数据,但是如果我们再细心一点,进一步搜索就会有惊喜。

通过直接在返回内容里搜索关键字,发现有一个js变量
window.__INITIAL_STATE__;
存储了页面所需数据。
在这里插入图片描述

而这个变量里的
['moment']['momentList']['moment_list']['datalist'][0]['datalist']
内容则就是新闻具体数据
在这里插入图片描述

接下来就简单了,同样的套路,分析请求必需参数和cookie反爬策略,然后我们通过请求后获取js变量结果方式来进行爬取。

二、代码实现

本次技术实现使用如下库:

1.playwright:用来打开URL,执行JavaScript代码,获取js变量值

源码如下

# -*- coding: utf-8 -*-

import os
import sys
import time

from playwright.sync_api import sync_playwright

opd = os.path.dirname
curr_path = opd(os.path.realpath(__file__))
proj_path = opd(opd(opd(curr_path)))
sys.path.insert(0, proj_path)

# http请求默认agent
USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'


spider_config = {
    "name_en": "https://www.huxiu.com/moment/",
    "name_cn": "虎嗅"
}


def extract_title(text):
    if text:
        first_sentence = str(text).split('。')[0]
        return first_sentence
    else:
        return text


class Huxiu:
    def __init__(self):
        self.headers = {
            'authority': 'www.huxiu.com',
            'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
            'user-agent': USERAGENT
        }

    def get_news(self):
        results = []
        with sync_playwright() as playwright:
            browser = playwright.chromium.launch(
                headless=True,
                slow_mo=1000,
                args=['--start-maximized']
            )

            context = browser.new_context(
                no_viewport=True,
                accept_downloads=True
            )
            page = context.new_page()
            page.set_default_timeout(200000)
            page.goto('https://www.huxiu.com/moment/')
            page.wait_for_load_state('load')
            # 获取动态JavaScript内容
            initial_state = page.evaluate('(function() { return window.__INITIAL_STATE__; })()')
            datalist = initial_state['moment']['momentList']['moment_list']['datalist'][0]['datalist']
            for data in datalist:
                results.append(
                    {
                        "news_title": extract_title(data['content']) + "。",
                        "news_date": data['format_time'],
                        "source_en": spider_config['name_en'],
                        "source_cn": spider_config['name_cn'],
                    }
                )

            browser.close()

        return results

def main():
    huxiu = Huxiu()
    results = huxiu.get_news()
    print(results)


if __name__ == '__main__':
    main()

源码中核心内容:获取动态JavaScript内容

initial_state = page.evaluate('(function() { return window.__INITIAL_STATE__; })()')

总结

1.分析页面,有些页面请求返回的是html,但是也有可能会将数据拼接在js里来渲染页面
2.Python中执行JavaScript代码一种推荐的方式是使用playwright这种库,内置浏览器引擎,且很少被认为是暴力请求,并且自带等待机制

本文章代码只做学习交流使用,作者不负责任何由此引起的法律责任。

各位看官,如对你有帮助欢迎点赞,收藏,转发

关注公众号【Python魔法师】带你了解更多Python魔法

qrcode.jpg

前言

在.NET应用开发中数据集的交互式显示是一个非常常见的功能,如需要创建折线图、柱状图、饼图、散点图等不同类型的图表将数据呈现出来,帮助人们更好地理解数据、发现规律,并支持决策和沟通。本文我们将一起来学习一下如何使用ScottPlot库在.NET WinForms中快速实现大型数据集的交互式显示。

ScottPlot类库介绍

ScottPlot是一个免费、开源(采用MIT许可证)的强大.NET交互式绘图库,能够轻松地实现大型数据集的交互式显示。使用几行代码即可快速创建折线图、柱状图、饼图、散点图等不同类型的图表。

ScottPlot类库支持平台和框架

Console Application、WinForms、WPF、Avalonia、Blazor、WinUI等多个平台和框架。

ScottPlot类库源代码

新建WinForms项目

新建一个名为
ScottPlotWinFormsExercise
的项目。

安装ScottPlot.WinForms包

搜索
ScottPlot.WinForms
包安装:

折线图实现

创建名为:
LineChart
窗体。


FormsPlot (ScottPlot.WinForms)
从工具箱拖到窗体中:

输入以下代码:

    public partial class LineChart : Form
    {
        public LineChart()
        {
            double[] dataX = GetRandomNum(20).Distinct().OrderByDescending(x => x).ToArray();
            double[] dataY = GetRandomNum(19).Distinct().OrderByDescending(x => x).ToArray();
            formsPlot1.Plot.Add.Scatter(dataX, dataY);
            formsPlot1.Refresh();
        }

        public double[] GetRandomNum(int length)
        {
            double[] getDate = new double[length];
            Random random = new Random(); //创建一个Random实例
            for (int i = 0; i < length; i++)
            {
                getDate[i] = random.Next(1, 100); //使用同一个Random实例生成随机数
            }
            return getDate;
        }
    }

运行效果展示:

柱状图实现

创建名为:
BarChart
窗体。


FormsPlot (ScottPlot.WinForms)
从工具箱拖到窗体中:

输入以下代码:

    public partial class BarChart : Form
    {
        public BarChart()
        {
            double[] values = { 5, 10, 7, 13, 22, 18, 33, 16 };
            formsPlot1.Plot.Add.Bars(values);
            formsPlot1.Refresh();
        }
    }

运行效果展示:

饼图实现

创建名为:
PieChart
窗体。


FormsPlot (ScottPlot.WinForms)
从工具箱拖到窗体中:

输入以下代码:

    public partial class PieChart : Form
    {
        public PieChart()
        {
            double[] values = { 3, 2, 8, 4, 8, 10 };
            formsPlot1.Plot.Add.Pie(values);
            formsPlot1.Refresh();
        }
    }

运行效果展示:

散点图实现

创建名为:
ScatterChart
窗体。


FormsPlot (ScottPlot.WinForms)
从工具箱拖到窗体中:

输入以下代码:

    public partial class ScatterChart : Form
    {
        public ScatterChart()
        {
            //从原始数据开始
            double[] xs = Generate.Consecutive(100);
            double[] ys = Generate.NoisyExponential(100);

            //对数据进行对数缩放,并处理负值
            double[] logYs = ys.Select(Math.Log10).ToArray();

            //将对数缩放的数据添加到绘图中
            var sp = formsPlot1.Plot.Add.Scatter(xs, logYs);
            sp.LineWidth = 0;

            //创建一个次要刻度生成器,用于放置对数分布的次要刻度
            ScottPlot.TickGenerators.LogMinorTickGenerator minorTickGen = new();

            //创建一个数值刻度生成器,使用自定义的次要刻度生成器
            ScottPlot.TickGenerators.NumericAutomatic tickGen = new();
            tickGen.MinorTickGenerator = minorTickGen;

            //创建一个自定义刻度格式化程序,用于设置每个刻度的标签文本
            static string LogTickLabelFormatter(double y) => $"{Math.Pow(10, y):N0}";

            //告诉我们的主要刻度生成器仅显示整数的主要刻度
            tickGen.IntegerTicksOnly = true;

            //告诉我们的自定义刻度生成器使用新的标签格式化程序
            tickGen.LabelFormatter = LogTickLabelFormatter;

            //告诉左轴使用我们的自定义刻度生成器
            formsPlot1.Plot.Axes.Left.TickGenerator = tickGen;

            //显示次要刻度的网格线
            var grid = formsPlot1.Plot.GetDefaultGrid();
            grid.MajorLineStyle.Color = Colors.Black.WithOpacity(.15);
            grid.MinorLineStyle.Color = Colors.Black.WithOpacity(.05);
            grid.MinorLineStyle.Width = 1;

            formsPlot1.Refresh();
        }
    }

运行效果展示:

项目演示入口

        private void Btn_ScatterChart_Click(object sender, EventArgs e)
        {
            ScatterChart formScatterChart = new ScatterChart();
            // 显示目标窗体
            formScatterChart.Show();
        }

        private void Btn_PieChart_Click(object sender, EventArgs e)
        {
            PieChart formPieChart = new PieChart();
            // 显示目标窗体
            formPieChart.Show();
        }

        private void Btn_BarChart_Click(object sender, EventArgs e)
        {
            BarChart formbarChart = new BarChart();
            // 显示目标窗体
            formbarChart.Show();
        }

        private void Btn_LineChart_Click(object sender, EventArgs e)
        {
            LineChart formLineChart = new LineChart();
            // 显示目标窗体
            formLineChart.Show();
        }

项目源码地址


更多项目实用功能和特性欢迎前往项目开源地址查看

1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

Keil µVision5 IDE(
MDK-Arm

野火DAP仿真器

XCOM V2.6串口助手

2、学习目标

本文主要学习 FreeRTOS 消息队列的相关知识,
包括消息队列概述、创建删除复位队列、写入/读取数据到队列等关于队列的基础知识

3、前提知识

3.1、什么是消息队列?

在一个实时操作系统构成的完整项目中一般会存在多个任务和中断,多个任务之间、任务与中断之间往往需要进行通信, FreeRTOS 中所有的通信与同步机制都是基于队列来实现的,我们可以把队列结构想象成如下图所示样子

在实际使用中,队列深度以及队列中数据类型都可以由用户自定义
,消息队列是一个共享的存储区域,其可以被多个进程写入数据,同时也可以被多个进程读取数据,为了让接收任务知道数据的来源,以确定数据应该如何处理,通常可以使用单个队列来传输具有两者的结构数据的值和结构字段中包含的数据源,如下图所示

3.2、创建队列

队列在使用前必须先创建
,和创建任务类似, FreeRTOS 也提供了动态或静态内存分配创建队列两个 API 函数,具体函数声明如下所示

/**
  * @brief  动态分配内存创建队列函数
  * @param  uxQueueLength:队列深度
  * @param  uxItemSize:队列中数据单元的长度,以字节为单位
  * @retval 返回创建成功的队列句柄,如果返回NULL则表示因内存不足创建失败
  */
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);

/**
  * @brief  静态分配内存创建队列函数
  * @param  uxQueueLength:队列深度
  * @param  uxItemSize:队列中数据单元的长度,以字节为单位
  * @param  pucQueueStorageBuffer:队列栈空间数组
  * @param  pxQueueBuffer:指向StaticQueue_t类型的用于保存队列数据结构的变量
  * @retval 返回创建成功的队列句柄,如果返回NULL则表示因内存不足创建失败
  */
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,
								 UBaseType_t uxItemSize,
								 uint8_t *pucQueueStorageBuffer,
								 StaticQueue_t *pxQueueBuffer);

/*example:创建一个深度为5,队列单元占uint16_t大小队列*/
QueueHandle_t QueueHandleTest;
QueueHandleTest = xQueueCreate(5, sizeof(uint16_t));

3.3、向队列写入数据

任务或者中断向队列写入数据称为发送消息
。通常情况下,队列被作为 FIFO(先入先出)使用,即数据由队列尾部进入,从队列首读出,当然可以通过更改写入方式将队列作为 LIFO(后入先出)使用,向队列中写入数据主要有三组 FreeRTOS API 函数,具体如下所示

/**
  * @brief  向队列后方发送数据(FIFO先入先出)
  * @param  xQueue:要写入数据的队列句柄
  * @param  pvItemToQueue:要写入的数据
  * @param  xTicksToWait:阻塞超时时间,单位为节拍数,portMAXDELAY表示无限等待
  * @retval pdPASS:数据发送成功,errQUEUE_FULL:队列满无法写入
  */
BaseType_t xQueueSend(QueueHandle_t xQueue,
					  const void * pvItemToQueue,
					  TickType_t xTicksToWait);

/**
  * @brief  向队列后方发送数据(FIFO先入先出),与xQueueSend()函数一致
  */
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,
							const void * pvItemToQueue,
							TickType_t xTicksToWait);

/**
  * @brief  向队列前方发送数据(LIFO后入先出)
  */
BaseType_t xQueueSendToFront(QueueHandle_t xQueue,
							 const void * pvItemToQueue,
							 TickType_t xTicksToWait);

/**
  * @brief  以下三个函数为上述三个函数的中断安全版本
  * @param  pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
  */
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,
							 const void *pvItemToQueue,
							 BaseType_t *pxHigherPriorityTaskWoken);

BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,
								   const void *pvItemToQueue,
								   BaseType_t *pxHigherPriorityTaskWoken)

BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,
									const void *pvItemToQueue,
									BaseType_t *pxHigherPriorityTaskWoken);

另外还有一组稍微特殊的向队列写入数据的 FreeRTOS API 函数,这组函数只用于队列长度为 1 的队列,在队列已满时会覆盖掉队列原来的数据,具体如下所述

/**
  * @brief  向长度为1的队发送数据
  * @param  xQueue:要写入数据的队列句柄
  * @param  pvItemToQueue:要写入的数据
  * @retval pdPASS:数据发送成功,errQUEUE_FULL:队列满无法写入
  */
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void *pvItemToQueue);

/**
  * @brief  以下函数为上述函数的中断安全版本
  * @param  pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
  */
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,
								  const void *pvItemToQueue,
								  BaseType_t *pxHigherPriorityTaskWoken);

3.4、从队列接收数据

任务或者中断从队列中读取数据称为接收消息
。从队列中读取数据主要有两组 FreeRTOS API 函数,具体如下所示

/**
  * @brief  从队列头部接收数据单元,接收的数据同时会从队列中删除
  * @param  xQueue:被读队列句柄
  * @param  pvBuffer:接收缓存指针
  * @param  xTicksToWait:阻塞超时时间,单位为节拍数
  * @retval pdPASS:数据接收成功,errQUEUE_FULL:队列空无读取到任何数据
  */
BaseType_t xQueueReceive(QueueHandle_t xQueue,
						 void *pvBuffer,
						 TickType_t xTicksToWait);

/**
  * @brief  从队列头部接收数据单元,不从队列中删除接收的单元
  */
BaseType_t xQueuePeek(QueueHandle_t xQueue,
					  void *pvBuffer,
					  TickType_t xTicksToWait);

/**
  * @brief  以下两个函数为上述两个函数的中断安全版本
  * @param  pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
  */
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,
								void *pvBuffer,
								BaseType_t *pxHigherPriorityTaskWoken);

BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue, void *pvBuffer);

3.5、查询队列

FreeRTOS 还提供了一些用于查询队列当前有效数组单元个数和剩余可用空间数的 API 函数,具体如下所述

/**
  * @brief  查询队列剩余可用空间数
  * @param  xQueue:被查询的队列句柄
  * @retval 返回队列中可用的空间数
  */
UBaseType_t uxQueueSpacesAvailable(QueueHandle_t xQueue);

/**
  * @brief  查询队列有效数据单元个数
  * @param  xQueue:被查询的队列句柄
  * @retval 当前队列中保存的数据单元个数
  */
UBaseType_t uxQueueMessagesWaiting(const QueueHandle_t xQueue);

/**
  * @brief  查询队列有效数据单元个数函数的中断安全版本
  */
UBaseType_t uxQueueMessagesWaitingFromISR(const QueueHandle_t xQueue);

3.6、阻塞状态

当出现下面几种情况时,任务会进入阻塞状态

  1. 当某个任务向队列写入数据,但是被写的队列已满时,任务将进入阻塞状态等待队列出现新的位置
  2. 当某个任务从队列读取数据,但是被读的队列是空时,任务将进入阻塞状态等待队列出现新的数据

当出现下面几种情况时,任务会退出阻塞状态

  1. 进入阻塞状态的任务达到设置的阻塞超时时间之后会退出阻塞状态
  2. 向满队列中写数据的任务等到队列中出现新的位置
  3. 从空队列中读数据的任务等到队列.中出现新的数据

当存在多个任务处于阻塞状态时,如果同时满足解除阻塞的条件,则所有等待任务中
优先级最高的任务 或者 优先级均相同但等待最久的任务
将被解除阻塞状态

3.7、删除队列

/**
  * @brief  删除队列
  * @param  pxQueueToDelete:要删除的队列句柄
  * @retval None
  */
void vQueueDelete(QueueHandle_t pxQueueToDelete);

3.8、复位队列

/**
  * @brief  将队列重置为其原始空状态
  * @param  xQueue:要复位的队列句柄
  * @retval pdPASS(从FreeRTOS V7.2.0之后)
  */
BaseType_t xQueueReset(QueueHandle_t xQueue);

3.9、队列读写过程

如下图展示了用作 FIFO 的队列写入和读取数据的情况的具体过程
(注释1)

4、实验一:尝试队列基本操作

4.1、实验目标

  1. 创建一个用于任务间、任务与中断间信息传输的深度为 10 的队列 TEST_QUEUE
  2. 创建一个任务 TASK_SEND 实现按键扫描响应,当 KEY2、KEY1、KEY0 按键按下时分别向队列 TEST_QUEUE 中发送不同消息
  3. 创建一个任务 TASK_RECEIVE 实现从队列 TEST_QUEUE 中接收信息,根据接收到的不同信息通过串口输出不同内容
  4. 启动一个 RTC 周期唤醒中断,每隔 1s 向队列 TEST_QUEUE 中发送一条消息

4.2、CubeMX相关配置

首先读者应按照 “
FreeRTOS教程1 基础知识
” 章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化开发板上 KEY2、KEY1和KEY0 用户按键做普通输入,具体配置步骤请阅读“
STM32CubeMX教程3 GPIO输入 - 按键响应
”,注意虽开发板不同但配置原理一致,如下图所示

本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信
”,如下图所示

本实验需要配置 RTC 周期唤醒中断,具体配置步骤和参数介绍读者可阅读”
STM32CubeMX教程10 RTC 实时时钟 - 周期唤醒、闹钟A/B事件和备份寄存器
“实验,此处不再赘述,这里参数、时钟配置如下图所示

由于需要在 RTC 周期唤醒中断中使用 FreeRTOS 的 API 函数,因此 RTC 周期唤醒中断的优先级应该设置在 15~5 之间,此处设置为 7 ,具体如下图所示

单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Tasks and Queues 选项卡,首先双击默认任务修改其参数,然后单击 Add 按钮按要求增加另外一个任务,具体如下图所示

然后在下方单击 Add 按钮增加一个深度为 10 的队列,具体如下图所示

配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

4.3、添加其他必要代码

按照 “
STM32CubeMX教程9 USART/UART 异步通信
” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述

首先应该在 freertos.c 中添加使用到的头文件,如下所述

#include "stdio.h"
#include "queue.h"

然后在 rtc.c 文件下方重新实现 RTC 的周期唤醒回调函数,在该函数体内发送数据 ”9“ 到队列 TEST_QUEUE 中,具体如下所述

/*周期唤醒回调函数*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
	uint16_t key_value = 9;
	BaseType_t pxHigherPriorityTaskWoken;
	//向队列中发送数据,中断安全版本
	xQueueSendToBackFromISR(TEST_QUEUEHandle, &key_value, &pxHigherPriorityTaskWoken);
	//进行上下文切换
	portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
}

最后在 freertos.c 中添加任务函数体内代码即可,任务 TASK_SEND 负责当有按键按下时发送不同的数据到队列 TEST_QUEUE 中,任务 TASK_RECEIVE 则负责当队列中有数据时从队列中读取数据并通过串口输出给用户 ,具体如下所述

/*发送任务*/
void TASK_SEND(void *argument)
{
  /* USER CODE BEGIN TASK_SEND */
	uint16_t key_value = 0;
  /* Infinite loop */
  for(;;)
  {
	key_value = 0;
	//按键KEY2按下
	if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
		key_value = 3;
	//按键KEY1按下
	if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
		key_value = 2;
	//按键KEY0按下
	if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
		key_value = 1;
	//如果有按键按下
	if(key_value != 0)
	{
		BaseType_t err = xQueueSendToBack(TEST_QUEUEHandle, &key_value, pdMS_TO_TICKS(50));
		
		if(err == errQUEUE_FULL)
		{
			xQueueReset(TEST_QUEUEHandle);
		}
		//按键消抖
		osDelay(300);
	}
	else
		osDelay(10);
  }
  /* USER CODE END TASK_SEND */
}

/*接收任务*/
void TASK_RECEIVE(void *argument)
{
  /* USER CODE BEGIN TASK_RECEIVE */
	UBaseType_t msgCount=0,freeSpace=0;
	uint16_t key_value=0;
  /* Infinite loop */
  for(;;)
  {
	msgCount = uxQueueMessagesWaiting(TEST_QUEUEHandle);
	freeSpace = uxQueueSpacesAvailable(TEST_QUEUEHandle);
	BaseType_t result = xQueueReceive(TEST_QUEUEHandle, &key_value, pdMS_TO_TICKS(50));
	
	if(result != pdTRUE)
		continue;
	
	printf("msgCount: %d, freeSpace: %d, key_value: %d\r\n", (uint16_t)msgCount, (uint16_t)freeSpace, key_value);
	
    osDelay(100);
  }
  /* USER CODE END TASK_RECEIVE */
}

4.4、烧录验证

烧录程序,打开串口助手,可以发现每隔一定时间 TASK_RECEIVE 任务会从队列中接收到 ”9“,当按键 KEY2 按下时 TASK_SEND 任务向队列中发送 ”3“,同时 TASK_RECEIVE 任务会从队列中接收到 ”3“ 表示任务 TASK_SEND 发送成功,同理按键 KEY1 按下时发送接收 ”2“ ,按键 KEY0 按下时发送接收 ”1“ ,整个过程串口输出信息如下图所示

5、注释详解

注释1
:图片来源于
Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

参考资料

STM32Cube高效开发教程(基础篇)

Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

ASP.NET Core 中的框架中发出大量诊断事件,包括
当前请求
进入请求完成事件,
HttpClient
发出收到与响应,
EFCore
查询等等。

我们可以利用DiagnosticListener来选择性地监听这些事件,然后通过自己的方式组织这些日志,实现
无侵入
的分布式跟踪。

下面我们通过DiagnosticSource监听EFCore,与HTTPClient,实现链路追踪。

创建监听

现在我们将配置一个DiagnosticListener来监听全部事件。

首先,我们需要一个IObserver<DiagnosticListener>,我们将使用它来订阅所有事件。

public class TestDiagnosticObserver : IObserver<DiagnosticListener>{public voidOnNext(DiagnosticListener value)
{
value.Subscribe(
newTestKeyValueObserver());
}
public voidOnCompleted() { }public voidOnError(Exception error) { }
}
其中重要的方法是OnNext。
然后我们传入另一个自定义类型TestKeyValueObserver,这是实际接收实例发出的事件的类DiagnosticListener。
该事件会接受KeyValuePair<string, object>参数,我们后续可针对此参数做业务相关的筛选。
public class TestKeyValueObserver : IObserver<KeyValuePair<string, object?>>{public void OnNext(KeyValuePair<string, object?>value)
{
var activity =Activity.Current;

Console.WriteLine($
"traceId {activity?.TraceId} Received event: {value.Key}");
}
public voidOnCompleted() { }public voidOnError(Exception error) { }
}

最后一步是在应用程序中注册我们的程序TestDiagnosticObserver。

DiagnosticListener.AllListeners.Subscribe(new TestDiagnosticObserver());

创建HTTP请求与EFCore查询

我们新建一个接口,用来集成EF与HttpClient。并调用这个接口查看DiagnosticListener 监听到的内容

[HttpGet]public async Task<string>GetAsync()
{
//HTTP await _httpClient.GetAsync("https://www.baidu.com");//EF Item item = newItem()
{
Barcode
=Guid.NewGuid().ToString(),
Brand
= "Milky Way",
Name
= "Milk",
PruchasePrice
= 20.5,
SellingPrice
= 25.5};
_productsContext.Items.Add(item);
_productsContext.SaveChanges();
return "OK";
}

调用此接口来看看我们的DiagnosticListener的效果。

可以看到收到了很多Event,包括
当前请求
的各个阶段,
HttpClient
的各个阶段,与
EFCore查询
的各个阶段。

解析Event

然后修改TestKeyValueObserver,我们从中挑选我们需要的HTTPClient与EFCore相关的事件。

public class TestKeyValueObserver : IObserver<KeyValuePair<string, object?>>{public void OnNext(KeyValuePair<string, object?>value)
{
var activity =Activity.Current;//Console.WriteLine($"traceId {activity?.TraceId} Received event: {value.Key}"); if (value.Key.StartsWith("System.Net.Http.Request"))
{
var cEventStr =JsonConvert.SerializeObject(value.Value);var cEvent = JsonConvert.DeserializeAnonymousType(cEventStr, new { Request = new { RequestUri = ""} , Timestamp = 2879029490722});
Console.WriteLine($
"traceId {activity?.TraceId} Request.Start: {cEvent.Timestamp}");
Console.WriteLine($
"traceId {activity?.TraceId} Request.Uri: {cEvent.Request.RequestUri}");
}
if (value.Key.StartsWith("System.Net.Http.Response"))
{
var cEventStr =JsonConvert.SerializeObject(value.Value);var cEvent = JsonConvert.DeserializeAnonymousType(cEventStr, new { Request = new { RequestUri = "" }, Timestamp = 2879029490722});
Console.WriteLine($
"traceId {activity?.TraceId} Http.Response: {cEvent.Timestamp}");
}
if (value.Key.StartsWith("Microsoft.EntityFrameworkCore.Database.Connection.ConnectionOpening"))
{
var cEvent =(Microsoft.EntityFrameworkCore.Diagnostics.ConnectionEventData)value.Value;
Console.WriteLine($
"traceId {activity?.TraceId} Connection.ConnectionOpening: {cEvent?.StartTime.ToString("yyyy-MM-dd HH:mm:ss:fff")}");
}
if (value.Key.StartsWith("Microsoft.EntityFrameworkCore.Database.Command.CommandExecuting"))
{
var cEvent =(Microsoft.EntityFrameworkCore.Diagnostics.CommandEventData)value.Value;
Console.WriteLine($
"traceId {activity?.TraceId} {cEvent?.Command.CommandText}");
}
if (value.Key.StartsWith("Microsoft.EntityFrameworkCore.Database.Connection.ConnectionClosed"))
{
var cEvent =(Microsoft.EntityFrameworkCore.Diagnostics.ConnectionEventData)value.Value;
Console.WriteLine($
"traceId {activity?.TraceId} Connection.ConnectionClosed: {cEvent?.StartTime.ToString("yyyy-MM-dd HH:mm:ss:fff")}");
}
}
public voidOnCompleted() { }public voidOnError(Exception error) { }
}

再次启动,查看效果,可以看到已经获取到了http请求的开始结束事件,EF的查询语句,开始事件等。

最后我们可以结构化这些数据,并将其持久化到自己的监控体系中,实现链路跟踪。