2024年3月

.NET Aspire是一个有态度的云原生应用开发框架,旨在改善生成.NET云原生应用的体验,并提供一组强大的工具来帮助你生成和运行分布式应用。它允许开发者快速创建属于自己的云原生应用,或改造已有的项目成为云原生应用。.NET Aspire提供服务发现、健康检查、故障和容错策略、Redis服务、可观测性以及遥测面板等功能,并展现出强大的可扩展性。.NET Aspire 提供了如下3个方面的能力,来帮助我们使用.NET开发分层、云就绪的可观测、本地与生产环境一致的分布式云原生应用程序:

  • 微服务的编排能力:在开发和线上环境自动处理多个微服务之间的连接和依赖,也即内置了服务发现等基础能力。

  • 基础服务的访问能力:通过提供一系列Nuget包,把对一些基础服务的访问进行统一封装(比如,缓存、数据库、消息队列等),方便微服务直接使用。

  • 工具链的相应配合:在VS和VSCode等工具中添加对以上编排能力的支持,并提供一系列开箱即用的模板,来方便开发人员进行开发、调试和部署云原生应用。

.NET Aspire Preview 4 现已发布!.NET Aspire预览版4引入了多项重要增强功能,包括社区高度需求的特性。该版本关注的领域包括对Entity Framework组件的改进、Podman支持以及应用模型方面的更改,以便轻松选择使用现有资源或预配新资源。.NET Aspire预览版4的版本号是8.0.0-preview.4.24156.9.以下是此预览版中新增内容的摘要:

  • Podman
    • 支持使用
      podman
      运行应用程序。Podman是一个无守护进程的容器引擎,用于在Linux系统上开发、管理和运行OCI容器。它是Linux用户运行容器而不需要守护进程的绝佳选择。如果同时检测到Docker和Podman,则优先使用Docker。可以通过环境变量
  • Dashboard  仪表板,仪表板的外观和感觉进行了更新,新的仪表板设计旨在减少导航标签所占用的空间,并简化在日志、指标和跟踪之间的导航。.NET Aspire仪表板现在可以作为独立容器镜像运行,这使得使用仪表板管理在不同机器或不同环境运行的应用程序变得更加容易。仪表板还可以作为OTLP grpc端点的收集器和查看器,用于希望发送和可视化遥测数据的应用程序
    • 独立容器镜像,参见示例
    • 辅助功能改进,包括
      Shift
      +
      ?
      快捷键
    • 指标表视图,在图表或表格之间切换
  • 数据库和实体框架改进,引入了对Entity Framework的新方法,用于配置Entity Framework。新的Enrich[Provider]DbContext()方法在注册DbContext类时提供了更多灵活性。这些方法不注册DbContext,而是在调用它们之前期望你自行注册。这些方法仍然配置命令重试、健康检查、日志和遥测,此外,预览版4改进了使用EF Core工具在.NET Aspire应用中创建迁移的过程。以前,EF Core工具会因缺少数据库连接字符串而失败。在预览版4中,.NET Aspire检测到项目是否使用EF Core工具启动,并禁用连接字符串验证,从而允许成功创建迁移。另一个挑战是与 transient 数据库应用迁移。为此,探索了在.NET Aspire解决方案中添加一个.NET后台工作者资源的方法。这个工作者在应用主机启动时执行迁移
    • 新的管理工具,MySQL (phpMyAdmin) 和 MongoDB (mongo-express)
    • 实体框架迁移问题已解决,请参阅
      示例
    • 数据库服务器资源更新
    • 数据库容器资源更新
    • 新的
      Enrich
      API,
      Enrich[Provider]DdContext
  • 外部参数,请参阅 .NET Aspire 文档:外部参数:这些参数用于表示在构建时未知且可能因环境而异的值。这些值在部署应用程序时会提示输入
  • New idioms:  新习语:
    • DistributedApplicationBuilder.ExecutionContext

      IsPublisherMode
    • PublishAs

      RunAs

      As
      API
  • API improvements  API 改进,在容器资源方面,预览版4引入了对容器资源的新方法,简化了容器资源的添加过程
    • 更改容器属性,
      WithImageTag
    • 拆分绑定和卷安装,
      WithVolumeMount

      WithBindMount
  • Components  成分
    • 新功能:Azure AI 搜索文档
    • 场景:.NET Aspire 对 Azure SignalR 服务的支持
    • 更新:.NET Aspire 与 Application Insights
  • 具有 Azure 资源映射的容器(还有更多计划)
    • Redis -> Azure Redis
    • Postgres -> Azure Database for PostgresSQL
    • SQL Server -> Azure SQL Server
      SQL 服务器 -> Azure SQL 服务器
  • Deployment  部署
  • Emulators updates  模拟器更新

    • UseEmulator
      更改为
      RunAsEmulator

有关详细信息,请参阅 .NET Aspire 文档:
.NET Aspire docs: Preview 4
.  总体而言,.NET Aspire预览版4通过引入新的特性和改进现有功能,为开发云原生应用程序提供了更多灵活性和便利性。

前言

今天大姚给大家分享一个.NET开源(MIT License)、快速、强大、免费的电子表格组件,支持数据格式、冻结、大纲、公式计算、图表、脚本执行等。兼容 Excel 2007 (.xlsx) 格式,支持WinForm、WPF和Android平台:ReoGrid。

项目特点

  • 功能丰富
    :ReoGrid 提供了强大而全面的电子表格功能,包括数据格式、公式计算、数据筛选、图表等。
  • 灵活性
    :ReoGrid 允许开发人员通过编程方式自定义和扩展其功能。它提供了灵活的 API 和事件模型,使得用户可以定制化各种行为和样式,满足个性化的需求。
  • 开源免费
    :ReoGrid 是一个开源项目,遵循 MIT 许可证。这意味着你可以免费使用和修改该库,同时也可以参与社区贡献和改进。
  • 支持多平台
    :ReoGrid支持WinForm、WPF和Android平台。

项目源码示例运行

设置WinForm
Demo
为启动项目运行:

WinForm快速接入

安装
unvell.ReoGrid
包:

安装成功工具箱会出现ReoGridControl控件:

将控件拖入到对应窗体中:

更多功能截图

项目源码地址



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

示例项目:
https://gitee.com/easyxaf/recharge-rules-engine-sample

前言

继上一篇文章对规则引擎编辑器进行了初步介绍之后,本文将通过实际应用案例深入探讨规则引擎编辑器的使用方法。编辑器的操作相对简单,我们将重点放在RulesEngine的讲解上。请注意,本文不是RulesEngine的入门教程,如果您对RulesEngine尚不熟悉,建议先行查阅其官方文档,
https://microsoft.github.io/RulesEngine

RulesEngine

这里要说一下在使用RulesEngine时的一些注意事项

RulesEngine中的Workflow类是规则信息的核心载体。它不仅包含了一个规则列表(Rules),而且每个Rule内部同样嵌套着一个规则列表。这样的设计形成了一个多层次的树状结构。然而,值得注意的是,在这个结构中,
只有叶节点的表达式会被实际执行
。也就是说,如果一个Rule内部的Rules列表非空,那么即使该Rule定义了表达式,它也不会被执行,它的运行结果由子Rule来决定。

对于嵌套的Rule(即子Rule),其执行方式可以通过NestedRuleExecutionMode进行配置。默认情况下,该模式设置为All,意味着所有规则都将被执行,而不考虑Rule中设置的运算符(Operator)。另一种模式是Performance,即性能模式,它会根据Rule中Operator的值来决定执行逻辑:当Operator为And或AndAlso时,如果任一子Rule返回false,则停止执行;当Operator为Or或OrElse时,如果任一子Rule返回true,则停止执行。这种模式是全局性的,适用于所有子Rule。需要注意的是,Workflow中的Rules是顶级Rule,不是嵌套Rule,不受这个设置的限制。除非有特殊需求,否则通常建议保持默认的All设置。后文将进一步介绍这两种模式的具体应用场景。

每个Rule都包含一个Actions属性,Actions同时又包含OnSuccess和OnFailure这两个子属性。需要注意的是,Workflow中的所有Rule执行完毕后,才会根据结果执行相应的OnSuccess或OnFailure动作。当Rule的结果IsSuccess为true时,将执行OnSuccess;反之,则执行OnFailure。RulesEngine内部默认提供了OutputExpressionAction和EvaluateRuleAction这两种动作。通过OutputExpressionAction,我们可以设置输出表达式。每个Rule都保存有自己的输出值,因此在规则执行完毕后,我们需要自行遍历并检索这些输出值,需要注意的是,输出结果只有一个Output属性,如果我们想区分不同的输出值,我们需要在Contenxt中设置类型信息,在读取值时再通过这个类型信息用于区分不同的值。

示例

在深入探讨之前,我想向大家推荐一个项目:
http://waitmoon.com/zh/guide
。这是一个基于Java语言开发的规则引擎,该项目的设计理念和功能实现在我设计规则引擎编辑器的过程中给予了我极大的启发。接下来的示例将借鉴它文档中的案例,以助于我们更好地理解和应用规则引擎的概念。如果您对规则引擎感兴趣,或者正在寻求灵感,这个项目绝对值得一看。

示例是一个充值活动,充值返现或送积分,我先从简单开始,一步步的丰富它。

上面是一个最简单的规则,"充100返现5元" 与 "充50送10积分" 这两个规则在RulesEngine是顶级规则,就是它们都会被执行,如果 "充100" 那两个优惠会被叠加。如果不想被叠加,我们需要给它们创建一个父规则,如下图

你会看到"充值活动"的操作符是"或"(OR),同时它底下有"一个"的字样,它还有一个选项是"全部",这是"嵌套规则输出方式",它主要针对OR操作符,这是扩展出来的功能,在上面的介绍中我们知道RulesEngine默认会执行所有规则,同时输出值会存储在每个规则结果中,这样我们可以取一个也可以取全部,你可以把"嵌套规则输出方式"看作是取输出值的标识,需要注意的是,AND操作符是没有这个选项的,因为只要一个子规则失败,父级规则就是失败的,所以也不会执行OnSuccess动作了。如上面的示例,取全部就是叠加。如下图

但这里有一个注意事项,前面提到的NestedRuleExecutionMode设置,如果设置为Performance,则上面的"全部"选项则不起作用,它只会执行一个,所以如果想更灵活的使用RulesEngine,建议使用默认设置,除非确认没有上面示例中的叠加场景。

下面我们再给这个规则加个日期限制,我们可以直接修改"充值活动"为"活动日期为10.1到10.7"

现在面临一个问题,我们是否可以为"活动日期为10.1到10.7",直接设定一个表达式呢?根据我们之前对RulesEngine的了解,它仅执行树状结构中的叶节点表达式。这意味着,对于"活动日期为10.1到10.7"这一节点,其内部的表达式不会被执行,除非它是叶节点。然而,如果我们有一个具有多层次节点的复杂规则结构,那么为每个叶节点添加父级规则的条件将变得异常繁琐。这不仅增加了配置的复杂性,还可能导致维护上的困难。因此,我们需要寻找一种更为高效和简洁的方法来处理这种情况,来简化规则的设置过程。RulesEngine的默认执行方式我们改变不了,但我们可以在编译规则之前对规则进行一次预处理。下面是预处理代码

public static void PreProcess(this Rule rule, Rule parentRule = null)
{
    if (!string.IsNullOrWhiteSpace(parentRule?.Expression))
    {
        if (!string.IsNullOrWhiteSpace(rule.Expression))
        {
            rule.Expression = $"({parentRule.Expression}) && ({rule.Expression})";
        }
        else
        {
            rule.Expression = parentRule.Expression;
        }
    }

    if (rule.Rules != null)
    {
        foreach (var childRule in rule.Rules.ToList())
        {
            PreProcess(childRule, rule);
        }
    }
}

通过上面的扩展方法,我们可以将父级的表达式与其合并,这样叶节点就可以拥有其父级表达式了。

那如果我们再给"充50送10积分"添加一个时间限制,如"活动日期为10.5到10.7",就非常简单了,添加"活动日期为10.5到10.7"节点并为其设置表达式就可以了,如下图

我们又有新的需求了,如果老客户在充值100元后,他会得到5积分,如下图

大家想想上面的规则可以吗?RulesEngine总是执行叶节点,这个一定要谨记。如果新客户充100元,"老客户送5积分"不会被执行,那"充100返5元"也不会被执行,最终是选择下面的节点。

这里我们有两个处理方案

1、在不改变"充100返5元"节点的情况下,直接在其下面创建一个子规则,子规则的表达式直接返回true,这样"老客户送5积分"返回false,也不影响"充100返5元"的执行,如下图

2、我们可以再优化一下,将"返现5元"放到子规则中,需要注意,当前操作符为"或",同时"嵌套规则输出方式"为"全部",如下图

关于规则创建的基本概念,我们的讨论就先进行到这里。请记住,无论规则逻辑多么复杂,它们都可以通过这些基本元素逐步组合起来。通过巧妙地拼接简单的规则节点,我们可以创造出功能强大、逻辑清晰的规则逻辑。

接下来,让我们探讨一下输出。在前述示例中,涉及到了两种输出类型:"现金"和"积分",我们可以在Workflow节点下配置相应的输出类型,配置完后,我们可以在输出表达式动作(OutputExpressionAction)中选择输出类型。如下图

输出表达式动作中的表达式,是 DynamicLinq的表达式语法
https://dynamic-linq.net/expression-language
,下面我们基于该表达式创建一个新的规则需求,如上面的示例"充100返5元",我们把它改为每充100返5元,也就是充值200直接返10元。如下图

通过上面的表达式就可以实现"每充100返5元"

当我们设置完输出后,我们如何在执行完规则后,获取到输出值呢,下面是结合输出类型获取输出值的代码,它会返回一个字典,Key是输出类型,Value是输出值列表(每一个成功的规则结果值),后续大家可以根据自己的业务逻辑组织这一些值,上述示例,我们是对"现金"返回最大值,对"积分"是求和。

public static Dictionary<string, List<object>> GetOutputResults(this RuleResultTree resultTree)
{
    var outputResults = new Dictionary<string, List<object>>();

    if (resultTree.IsSuccess)
    {
        if (resultTree.ActionResult?.Output != null)
        {
            var context = resultTree.Rule.Actions.OnSuccess.Context;
            var outputType = context.GetValueOrDefault("type", "default") as string;
            if (!outputResults.ContainsKey(outputType))
            {
                outputResults[outputType] = [];
            }
            outputResults[outputType].Add(resultTree.ActionResult.Output);
        }
    }

    if (resultTree.ChildResults != null)
    {
        var outputMode = resultTree.Rule.Properties?.GetValueOrDefault("nestedRuleOutputMode") as string;
        foreach (var childResult in resultTree.ChildResults)
        {
            var childOutputResults = GetOutputResults(childResult);

            foreach (var childOutputResult in childOutputResults)
            {
                if (!outputResults.ContainsKey(childOutputResult.Key))
                {
                    outputResults[childOutputResult.Key] = [];
                }
                outputResults[childOutputResult.Key].AddRange(childOutputResult.Value);
            }

            if (childOutputResults.Any() && outputMode == "one")
            {
                break;
            }
        }
    }

    return outputResults;
}

下面是对输出值的处理

var outputResults = ruleResults.First().GetOutputResults();

Console.Write("共返");

if (outputResults.TryGetValue("现金", out List<object> moneyList))
{
    var money = moneyList.Select(m => double.Parse(m.ToString())).Max();
    Console.Write($"  {money}元现金");
}

if (outputResults.TryGetValue("积分", out List<object> scoreList))
{
    var score = scoreList.Select(m => double.Parse(m.ToString())).Sum();
    Console.Write($"  {score}积分");
}

写在最后

RulesEngine是一款轻量的规则引擎类库,它不仅提供了一套核心的基础功能,而且其设计具有卓越的扩展性。这使得开发者得以在此基础上构建更为强大和定制化的功能,满足各种复杂的业务逻辑需求。然而,手动编辑RulesEngine的规则文件无疑是一项耗时且繁琐的任务。正是为了减轻这一工作负担,开发规则编辑器的想法应运而生。编辑器的引入旨在简化规则的创建和管理过程,使得规则的维护变得更加高效和直观,从而将开发者从重复且繁杂的手工编辑工作中解放出来。

https://www.cnblogs.com/haoxj/p/18073710

1、准备材料

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

STM32CubeMX软件(
Version 6.10.0

Keil µVision5 IDE(
MDK-Arm

野火DAP仿真器

XCOM V2.6串口助手

一个滑动变阻器

2、学习目标

本文主要学习 FreeRTOS 信号量的相关知识,
包括创建/删除信号量、释放信号量、获取信号量等知识

3、前提知识

3.1、信号量概述

信号量是进程间用于通信的一种手段,其是基于队列实现的,信号量更适用于进程间同步,信号量包括二值信号量(Binary Semaphores)和计数信号量(Counting Semaphores)

二值信号量就是只有一个项的队列,该队列不为空则为满(所谓二值),二值信号量就像一个标志,适和用于进程间同步的通信

举个例子:ADC 的周期采集中断负责采集完成后将采集到的 ADC 的值写入数据缓存区中并且释放信号量,总是尝试获取信号量的数据处理任务在 ADC 采集中断释放信号量之后成功获取,然后退出阻塞状态对写入数据缓存区中采集到的 ADC 值进行处理,上述过程如下图所示:
(注释1)

如下图所示为使用二值信号量来同步任务和中断的工作流程
(注释2)

计数信号量就是有固定长度的队列,队列中每个单元都是一个标志,其通常用于对多个共享资源的访问进行控制

举个例子:一家餐馆有 4 张可供用餐的桌子,我们创建一个长度为 4 ,初值为 4 的计数信号量来表示当前可供用餐的桌子数量,当有客人进来用餐时会 “获取” 一张餐桌,这时用来表示可供用餐的桌子数量的计数信号量就会减少一个,当有客人离开时会 “释放” 一张餐桌,这时用来表示可供用餐的桌子数量的计数信号量就会增加一个,上述过程如下图所示:

如下图所示为计数信号量的工作流程

3.2、创建信号量

信号量在使用之前也必须先创建
,信号量被创建完之后是无效的,也即为 0 ,而由于信号量分为二值信号量和计数信号量两种,因此FreeRTOS也提供了不同的API函数,具体如下所述

/**
  * @brief  动态分配内存创建二值信号量函数
  * @param  xSemaphore:创建的二值信号量句柄
  * @retval None
  */
void vSemaphoreCreateBinary(SemaphoreHandle_t xSemaphore);

/**
  * @brief  静态分配内存创建二值信号量函数
  * @param  pxSemaphoreBuffer:指向一个StaticSemaphore_t类型的变量,该变量将用于保存信号量的状态
  * @retval 返回创建成功的信号量句柄,如果返回NULL则表示因为pxSemaphoreBuffer为空无法创建
  */
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(
									StaticSemaphore_t *pxSemaphoreBuffer);

/**
  * @brief  动态分配内存创建计数信号量函数
  * @param  uxMaxCount:可以达到的最大计数值
  * @param  uxInitialCount:创建信号量时分配给信号量的计数值
  * @retval 返回创建成功的信号量句柄,如果返回NULL则表示内存不足无法创建
  */
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, 
										   UBaseType_t uxInitialCount);

/**
  * @brief  静态分配内存创建计数信号量函数
  * @param  uxMaxCount:可以达到的最大计数值
  * @param  uxInitialCount:创建信号量时分配给信号量的计数值
  * @param  pxSempahoreBuffer:指向StaticSemaphore_t类型的变量,该变量然后用于保存信号量的数据结构体
  * @retval 返回创建成功的信号量句柄,如果返回NULL则表示因为pxSemaphoreBuffer为空无法创建
  */
SemaphoreHandle_t xSemaphoreCreateCountingStatic(
									UBaseType_t uxMaxCount,
									UBaseType_t uxInitialCount,
									StaticSemaphore_t pxSempahoreBuffer);

3.3、释放信号量

以下两个函数不仅仅可以用于释放二值信号量,还可以用于释放计数信号量和互斥量,具体如下所示

/**
  * @brief  释放信号量函数
  * @param  xSemaphore:要释放的信号量的句柄
  * @retval 如果信号量释放成功,则返回pdTRUE;如果发生错误,则返回pdFALSE
  */
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);

/**
  * @brief  释放信号量的中断安全版本函数
  * @param  pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
  * @retval 如果成功给出信号量,则返回pdTRUE,否则errQUEUE_FULL
  */
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, 
								 BaseType_t *pxHigherPriorityTaskWoken);

3.4、获取信号量

以下两个函数不仅仅可以用于获取二值信号量,还可以用于获取计数信号量和互斥量,具体如下所示

/**
  * @brief  获取信号量函数
  * @param  xSemaphore:正在获取的信号量的句柄
  * @param  xTicksToWait:等待信号量变为可用的时间
  * @retval 成功获得信号量则返回pdTRUE;如果xTicksToWait过期,信号量不可用,则返回pdFALSE
  */
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
/**
  * @brief  获取信号量的中断安全版本函数
  * @param  xSemaphore:正在获取的信号量的句柄
  * @param  pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
  * @retval 成功获取则返回pdTRUE,未成功获取则返回pdFALSE
  */
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, 
								 signed BaseType_t *pxHigherPriorityTaskWoken);

3.5、删除信号量

/**
  * @brief  删除信号量,包括互斥锁型信号量和递归信号量
  * @param  xSemaphore:被删除的信号量的句柄
  * @retval None
  */
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);

3.6、工具函数

/**
  * @brief  获取信号量计数
  * @param  xSemaphore:正在查询的信号量的句柄
  * @retval 如果信号量是计数信号量,则返回信号量的当前计数值。如果信号量是二进制信号量,则当信号量可用时,返回1,当信号量不可用时,返回 0
  */
UBaseType_t uxSemaphoreGetCount(SemaphoreHandle_t xSemaphore);

4、实验一:二值信号量的应用

4.1、实验目标

  1. 创建一个二值信号量 BinarySem_ADC
  2. 配置 ADC1 IN5 在 500ms 的定时器驱动下周期采集 ADC 值,采集完成后将采集值写入缓存数组,然后释放二值信号量 BinarySem_ADC
  3. 创建一个任务 TASK_ADC,该任务总是尝试获取二值信号量 BinarySem_ADC,获取成功之后,将写入缓存数组的 ADC 采集值进行转换,然后通过 USART1 输出给用户

4.2、CubeMX相关配置

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

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

本实验需要设置 TIM3 作为 ADC1 IN5 触发源的单通道 ADC 采集,采集周期为 500ms ,因此需要配置 ADC1 和 TIM3,感兴趣读者可以阅读”
STM32CubeMX教程13 ADC - 单通道转换
“实验,如下图所示

由于我们将要在 ADC 采集完成中断中使用 FreeRTOS 的释放二值信号量的函数,
因此需要将其优先级设置在 15~5 之间,在这里设置为 7
,具体如下图所示

单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Tasks and Queues 选项卡双击默认任务修改其参数,具体如下图所示

然后在 Configuration 中单击 Timers and Semaphores ,在 Binary Semaphores 中单击 Add 按钮新增加一个名为 BinarySem_ADC 的二值信号量,具体如下图所示

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

4.3、添加其他必要代码

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

首先应该在 freertos.c 中添加信号量的头文件,如下所述

/*freertos.c中添加头文件*/
#include "semphr.h"

然后在该文件中重新实现 ADC 采集完成中断回调函数,在该函数中获取采集完成的 ADC 值,将其保存在全局变量 adc_value 中,然后释放二值信号量 BinarySem_ADC ,如下所述

/*转换完成中断回调*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    /*定时器中断启动单通道转换*/
    if(hadc->Instance == ADC1)
    {
        adc_value = HAL_ADC_GetValue(hadc);
		BaseType_t highTaskWoken = pdFALSE;
		if(BinarySem_ADCHandle != NULL)
		{
			xSemaphoreGiveFromISR(BinarySem_ADCHandle, &highTaskWoken);
			portYIELD_FROM_ISR(highTaskWoken);
		}
    }
}

接下来仍然在该文件中实现任务 TASK_ADC 的函数体内容,该任务函数总是尝试获取二值信号量,一旦获取成功表示 ADC 转换完成,就将 ADC 转换完成的值变为电压值,然后通过 USART1 输出给用户显示,如下所述

/*ADC任务函数*/
void TASK_ADC(void *argument)
{
	/* USER CODE BEGIN TASK_ADC */
	/* Infinite loop */
	for(;;)
	{
		if(xSemaphoreTake(BinarySem_ADCHandle, portMAX_DELAY) == pdTRUE)
		{
			uint32_t Volt = (3300 * adc_value)>>12;
			printf("val:%d, Volt:%d\r\n", adc_value, Volt);
		}
	}
	/* USER CODE END TASK_ADC */
}

最后在 main.c 文件主函数 main() 中以中断方式启动 ADC 转换即可,如下所述

//以中断方式启动ADC1
HAL_ADC_Start_IT(&hadc1);
//启动ADC1触发源定时器TIM3
HAL_TIM_Base_Start(&htim3);

4.4、烧录验证

烧录程序,打开串口助手,可以发现每隔一段时间就会输出当前 ADC1 IN5 通道采集到的 ADC 的值,将其接入一个滑动变阻器,当滑动变阻器从一端滑动到另一端时,串口输出的采集值也在从 0 逐渐变为最大值 4095 ,整个过程串口输出信息如下图所示

5、实验二:计数信号量的应用

5.1、实验目标

  1. 创建一个计数信号量 CountingSem_Tables ,设置最大值为 5 ,初始值设为 5 ,表示饭店内初始有 5 张桌子
  2. 启动 RTC 周期唤醒中断,唤醒周期为 3s ,在 RTC 唤醒中断中释放信号量,模拟有客人离开饭店
  3. 创建任务 TASK_KEY2 ,当按键 KEY2 按下时尝试获取信号量,模拟客人进店,同时调用获取信号量计数查询函数,查询当前可用餐桌个数

5.2、CubeMX相关配置

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

本实验需要初始化开发板上 KEY2 用户按键做普通输入,具体配置步骤请阅读 “
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 选项卡,双击默认任务修改其参数,具体如下图所示

然后在 Configuration 中单击 Timers and Semaphores 选项卡,在最下方的 Counting Semaphores 中单击右下角的 Add 按钮增加一个计数信号量,具体如下图所示

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

5.3、添加其他必要代码

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

首先应该在 freertos.c 中添加信号量的头文件,如下所述

/*freertos.c中添加头文件*/
#include "stdio.h"
#include "semphr.h"

然后在该文件中重新实现周期唤醒回调函数,该函数用于周期释放计数信号量,具体如下所示

/*周期唤醒回调函数*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
	if(CountingSem_TablesHandle != NULL)
	{
		BaseType_t highTaskWoken = pdFALSE;
		//释放计数信号量
		xSemaphoreGiveFromISR(CountingSem_TablesHandle, &highTaskWoken);
		portYIELD_FROM_ISR(highTaskWoken);
	}
}

最后仍然在该文件中实现任务 TASK_KEY2 ,该任务负责当按键 KEY2 按下时尝试获取计数信号量,当无任何按键按下时不断输出当前计数信号量可用数量,具体如下所示

void TASK_KEY2(void *argument)
{
  /* USER CODE BEGIN TASK_KEY2 */
  /* Infinite loop */
  for(;;)
  {
	if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
	{
		//获取计数信号量
		BaseType_t result = xSemaphoreTake(CountingSem_TablesHandle, pdMS_TO_TICKS(100));
		if(result == pdTRUE) printf("Check In OK\r\n");
		else printf("Check In Fail\r\n");
		//按键消抖
		osDelay(pdMS_TO_TICKS(300));
	}
	else
	{
		UBaseType_t AvailableTables = uxSemaphoreGetCount(CountingSem_TablesHandle);
		printf("Now AvailableTables is : %d\r\n", (uint16_t)AvailableTables);
		osDelay(pdMS_TO_TICKS(10));	
	}
  }
  /* USER CODE END TASK_KEY2 */
}

5.4、烧录验证

烧录程序,打开串口助手,发现不断输出当前可用计数信号量数量,当按住按键 KEY2 不松开,连续模拟客人进店,可以发现在模拟 3 个客人进店之后,剩余可用计数信号量的数量变为了 2 个,然后每隔一段时间计算信号量的数量慢慢增加直到最大值 5 ,接着按住按键 KEY2 不松开,连续模拟 5 个客人进店,当 5 个客人进店之后,再次按下 KEY2 按键可以发现串口输出 ”Check In Fail“,表示当前已无剩余可用计数信号量,上述整个过程如下图所示

6、注释详解

注释1
:图片来源于
STM32Cube高效开发教程(高级篇)
第五章

注释2
:图片来源于
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

一、分析页面

打开今日BBNews网址
https://news.bicido.com
,下拉选择【编程】栏目

首页.png

1.1、分析请求

F12打开开发者模式,然后点击Network后点击任意一个请求,Ctrl+F开启搜索,输入标题
Apache Doris 2.1.0 版本发布
,开始搜索

分析请求.png

搜索结果显示直接返回的json格式,那就so easy了,直接copy curl,然后将curl 转换为Python代码,运行。

推荐个curl转Python代码的在线工具:
https://curlconverter.com/

curl_to_python.png

二、代码实现

直接将curl 转换后的Python代码做下修改,然后调试运行即可。

完整代码

# -*- coding: utf-8 -*-
import os
import sys
from datetime import datetime

import requests

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

from app.conf.conf_base import USERAGENT

spider_config = {
    "name_en": "https://news.bicido.com",
    "name_cn": "今日BBNews"
}


class Bbnews:
    def __init__(self):
        self.headers = {
            'referer': 'https://news.bicido.com/',
            'user-agent': USERAGENT
        }

    def get_group(self):
        url = 'https://news.bicido.com/api/config/news_group/'
        content = requests.get(url=url, headers=self.headers)
        content = content.json()
        return content

    def get_news(self):
        groups = self.get_group()
        news_type = []
        for group in groups:
            if group['name'] == '编程':
                news_type = group['news_types']
        result = []
        for news_type in news_type:
            type_id = news_type['id']
            url = f'https://news.bicido.com/api/news/?type_id={type_id}'
            content = requests.get(url, headers=self.headers)
            news_list = content.json()
            for new in news_list:
                result.append({
                    "news_title": str(new['title']),
                    "news_date": datetime.now(),
                    "source_en": spider_config['name_en'],
                    "source_cn": spider_config['name_cn'],
                })
        return result


def main():
    bbnews = Bbnews()
    results = bbnews.get_news()
    print(results)


if __name__ == '__main__':
    main()

总结

  1. 今日BBNews页面没反爬策略,比较简单,拿来即用
  2. 本文介绍了curl to Python的工具,方便好用。

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

各位看官,如对你有帮助欢迎点赞,收藏,转发,关注公众号【Python魔法师】获取更多Python魔法~

qrcode.jpg