区块链系统开发测试----链码部署开发、系统开发验证

一.检查配置环境

检查虚拟机环境,确保有正在运行的Hyperledger Fabric区块链,并且其中chaincode_basic、credit_chaincode链码可以正常调用

查看chaincode_basic、credit_chaincode链码调用

二.开发征信链码代码 

基于现有征信链码,开发征信链码的升级版,使用chaincode-init文件夹中的基础链码模板,创建完善其中lib目录以及index.js内容,在lib中创建CreditPlusContract对象,在对象中添加createCreditSubjectPlus功能,实现征信主体的保存,定义subject变量属性包括(key:征信主体主键,  organizationName:征信主体评价机构名, type:评价类型,score:征信积分,creator:创建人,datetime:评价时间),其中属性除score外其余都为string类型。将subject内容上链保存  

在链码中添加征信主体查询功能(queryCreditSubjectPlus),要求能够查询所有主体内容(包括:key:征信主体主键,  organizationName:征信主体评价机构名, type:评价类型,score:征信积分,creator:创建人,datetime:评价时间)

'use strict';
const { Contract } = require("fabric-contract-api");
class CreditPlusContract extends Contract {
    async createCreditSubjectPlus(ctx, key, organizationName, type, creator, datetime) {
        console.info('=== START:创建征信主体 ===');
        const subject = {
            key: key,
            organizationName: organizationName,
            type: type,
            score: 0,
            creator: creator,
            datetime: datetime
        };
        await ctx.stub.putState(key, Buffer.from(JSON.stringify(subject)));
        console.info('=== END:创建征信主体 ===');
        return subject;
    }
    // 查询征信主体
    async queryCreditSubjectPlus(ctx, subjectKey) {
        console.info('=== START : 查询征信主体 ===');
        const bytes = await ctx.stub.getState(subjectKey);
        if (!bytes || bytes.length === 0) {
            const msg = `${subjectKey} 征信主体不存在`;
            console.warn(msg);
            throw new Error(msg);
        }
        const subject = JSON.parse(bytes.toString());
        console.info('=== END : 查询征信主体 ===');
        return subject;
    }
    
}

module.exports = CreditPlusContract;

 在链码结构中test目录中添加对于createCreditSubjectPlus以及QueryCreditSubjectPlus功能的单元测试,提交测试代码和验证结果。

'use strict';
const sinon = require('sinon');
const chai = require('chai');
const sinonChai = require('sinon-chai');
const expect = chai.expect;
chai.use(sinonChai);
let assert = sinon.assert;
const { Context } = require('fabric-contract-api');
const { ChaincodeStub, ClientIdentity } = require('fabric-shim');
const CreditPlusContract = require('../lib/creditContract');
describe('Credit Chaincode Test', () => {
    let stub, ctx, ClientId;

    beforeEach(() => {
        ctx = new Context();
        stub = sinon.createStubInstance(ChaincodeStub);
        stub.getMspID.returns('Org1');
        ctx.setChaincodeStub(stub);

        ClientId = sinon.createStubInstance(ClientIdentity);

        stub.putState.callsFake((key, value) => {
            if (!stub.states) {
                stub.states = {};
            }
            stub.states[key] = value;
        });
        stub.getState.callsFake(async (key) => {
            let ret;
            if (stub.states) {
                ret = stub.states[key];
            }
            return Promise.resolve(ret);
        });
        stub.deleteState.callsFake(async (key) => {
            if (stub.states) {
                delete stub.states[key];
            }
        });
    });
    describe('Test CreditSubject function', () => {
        it('should return success on createCreditSubject', async () => {
            let creditContract = new CreditPlusContract();
            let creditSubject = await creditContract.createCreditSubjectPlus(ctx, "A001", "My Company", "Company","Admin","2024-05-28 14:35:00");
            let scroe = creditSubject.score;
            expect(scroe).to.equals(0);
        });
        it('should return success on queryCreditSubject', async () => {
            let creditContract = new CreditPlusContract();
            await creditContract.createCreditSubjectPlus(ctx, "A001", "My Company", "Company", "Admin", "2024-05-28 14:35:00");
            let creditSubject = await creditContract.queryCreditSubjectPlus(ctx, "A001");
            let name = creditSubject.organizationName;
            expect(name).to.equals("My Company");
        });
    });
})

测试代码:

 

三.部署征信链码

在虚拟机指定Hyperledger Fabric中实现对应链码的部署并验证部署情况。

将链码名修改切credit_chaincode_plus,删除node_modules目录以及对应package-lock.json文件,上传至服务器chaincode目录下:

打包测试:

export FABRIC_CFG_PATH=${PWD}/config

peer lifecycle chaincode package ./chaincode/credit_chaincode_plus.tar.gz --path ./chaincode/credit_chaincode_plus --lang node --label credit_chaincode_plus_1.0

 

查看打包结果:

安装链码:运行以下进入fabric-cli容器:

docker exec -it fabric-cli bash

1. 在org1中安装:运行以下链码安装:

. scripts/set-env.sh 1 0 7051
peer lifecycle chaincode install chaincode/credit_chaincode_plus.tar.gz

 

2. 在org2中安装,运行以下链码安装:

. scripts/set-env.sh 2 0 9051
peer lifecycle chaincode install chaincode/credit_chaincode_plus.tar.gz

 

3. 查看安装情况

peer lifecycle chaincode queryinstalled

 

批准链码
org1批准链码

1. 设置链码环境变量

export CC_PACKAGE_ID=credit_chaincode_plus_1.0:9415a0be8812a91c2e510619a4d2a6a5cd06a8cf8f9cef96ee4eec2f456ab7ec

2. 设置Org1环境变量

. scripts/set-env.sh 1 0 7051

3.批准链码

peer lifecycle chaincode approveformyorg -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com --channelID $CHANNEL_NAME --name credit_chaincode_plus --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile $ORDERER_CA

 

1. 设置Org2环境变量

. scripts/set-env.sh 2 0 9051

2.批准链码

peer lifecycle chaincode approveformyorg -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com --channelID $CHANNEL_NAME --name credit_chaincode_plus --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile $ORDERER_CA

 

检查提交准备

peer lifecycle chaincode checkcommitreadiness --channelID $CHANNEL_NAME --name credit_chaincode_plus --version 1.0 --sequence 1 --tls --cafile $ORDERER_CA --output json

 

提交链码

peer lifecycle chaincode commit -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com --channelID $CHANNEL_NAME --name credit_chaincode_plus --version 1.0 --sequence 1 --tls --cafile $ORDERER_CA --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles $PEER0_ORG1_CA --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles $PEER0_ORG2_CA

 

查询提交的链码

peer lifecycle chaincode querycommitted --channelID $CHANNEL_NAME --name credit_chaincode_plus --tls --cafile $ORDERER_CA

 

查看运行镜像形成容器情况

docker logs -f b1ddd21303a5

 

调用createCreditSubject功能

peer chaincode invoke -o orderer.example.com:7050 --tls --cafile $ORDERER_CA --channelID $CHANNEL_NAME --name credit_chaincode_plus --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles $PEER0_ORG1_CA --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles $PEER0_ORG2_CA -c '{"function":"createCreditSubjectPlus", "Args":["A001", "My Company", "Company","Admin","2024-05-28 14:35:00"]}'

 

调用queryCreditSubject功能

peer chaincode query -C $CHANNEL_NAME --name credit_chaincode_plus -c '{"function":"queryCreditSubjectPlus","Args":["A001"]}'

 

四.部署删除和更新代码 

在链码中添加DeleteCreditSubject,实现按subject变量的key属性删除数据功能

在链码中添加UpdateSubjectScore功能,实现按key更新subject,更新subject变量中的score

'use strict';

const { Contract } = require("fabric-contract-api");

class CreditPlusContract extends Contract {
    async createCreditSubjectPlus(ctx, key, organizationName, type, creator, datetime) {
        console.info('=== START:创建征信主体 ===');
        const subject = {
            key: key,
            organizationName: organizationName,
            type: type,
            score: 0,
            creator: creator,
            datetime: datetime
        };
        await ctx.stub.putState(key, Buffer.from(JSON.stringify(subject)));
        console.info('=== END:创建征信主体 ===');
        return subject;
    }
    // 查询征信主体
    async queryCreditSubjectPlus(ctx, subjectKey) {
        console.info('=== START : 查询征信主体 ===');
        const bytes = await ctx.stub.getState(subjectKey);
        if (!bytes || bytes.length === 0) {
            const msg = `${subjectKey} 征信主体不存在`;
            console.warn(msg);
            throw new Error(msg);
        }
        const subject = JSON.parse(bytes.toString());
        console.info('=== END : 查询征信主体 ===');
        return subject;
    }
    async deleteCreditSubject(ctx, id) {
        const exists = await this.creditSubjectExists(ctx, id);
        if (!exists) {
            throw new Error(`The asset ${id} does not exist`);
        }
        return ctx.stub.deleteState(id);
    }

    async creditSubjectExists(ctx, id) {
        const assetJSON = await ctx.stub.getState(id);
        return assetJSON && assetJSON.length > 0;
    }
    async updateSubjectScore(ctx,subjectKey,inputScore) {
        const exists = await this.creditSubjectExists(ctx,subjectKey);
        if (!exists) {
            throw new Error(`The credit subject ${subjectKey} does not exist`);
        }
        const bytes = await ctx.stub.getState(subjectKey);
        if (!bytes || bytes.length ===0){
            const msg = `${subjectKey} 征信主体不存在`;
            console.warn(msg)
            throw new Error (msg);
        }
        var subject = JSON.parse(bytes.toString());
        subject.score=inputScore;
        return ctx.stub.putState(subjectKey, Buffer.from(JSON.stringify(subject)));
    }
    
}

module.exports = CreditPlusContract;

在链码结构中test目录中添加对于DeleteCreditSubject以及UpdateSubjectScore功能的单元测试 

 

'use strict';
const sinon = require('sinon');
const chai = require('chai');
const sinonChai = require('sinon-chai');
const expect = chai.expect;
chai.use(sinonChai);
let assert = sinon.assert;
const { Context } = require('fabric-contract-api');
const { ChaincodeStub, ClientIdentity } = require('fabric-shim');
const CreditPlusContract = require('../lib/creditContract');
describe('Credit Chaincode Test', () => {
    let stub, ctx, ClientId;

    beforeEach(() => {
        ctx = new Context();
        stub = sinon.createStubInstance(ChaincodeStub);
        stub.getMspID.returns('Org1');
        ctx.setChaincodeStub(stub);

        ClientId = sinon.createStubInstance(ClientIdentity);

        stub.putState.callsFake((key, value) => {
            if (!stub.states) {
                stub.states = {};
            }
            stub.states[key] = value;
        });
        stub.getState.callsFake(async (key) => {
            let ret;
            if (stub.states) {
                ret = stub.states[key];
            }
            return Promise.resolve(ret);
        });
        stub.deleteState.callsFake(async (key) => {
            if (stub.states) {
                delete stub.states[key];
            }
        });
    });
    describe('Test CreditSubject function', () => {
        it('should return success on createCreditSubject', async () => {
            let creditContract = new CreditPlusContract();
            let creditSubject = await creditContract.createCreditSubjectPlus(ctx, "A001", "My Company", "Company","Admin","2024-05-28 14:35:00");
            let scroe = creditSubject.score;
            expect(scroe).to.equals(0);
        });
        it('should return success on queryCreditSubject', async () => {
            let creditContract = new CreditPlusContract();
            await creditContract.createCreditSubjectPlus(ctx, "A001", "My Company", "Company", "Admin", "2024-05-28 14:35:00");
            let creditSubject = await creditContract.queryCreditSubjectPlus(ctx, "A001");
            let name = creditSubject.organizationName;
            expect(name).to.equals("My Company");
        });
        it('should return sucess on DeleteCreditSubject', async () => {
            let creditContract = new CreditPlusContract();
            await creditContract.createCreditSubjectPlus(ctx, "A001", "My Company", "Company", "Admin", "2024-05-28 14:35:00");
            await creditContract.deleteCreditSubject(ctx,"A001");
            let ret = await stub.getState('A001');
            expect(ret).to.equal(undefined);
        });
        it('should return sucess on updateSubjectScore', async () => {
            let creditContract = new CreditPlusContract();
            await creditContract.createCreditSubjectPlus(ctx, "A001", "My Company", "Company", "Admin", "2024-05-28 14:35:00");
            await creditContract.updateSubjectScore(ctx, "A001",10);
            let ret = JSON.parse(await stub.getState('A001'));
            expect(ret.score).to.eql(10);
        });
    });
})

测试代码:

 

五.更新部署征信链码

在链码中重新部署开发链码,实现链码更新(要求version、sequence有迭代痕迹)

打包测试:

export FABRIC_CFG_PATH=${PWD}/config

peer lifecycle chaincode package ./chaincode/credit_chaincode_plus.tar.gz --path ./chaincode/credit_chaincode_plus --lang node --label credit_chaincode_plus_1.1

 

安装链码
运行以下进入fabric-cli容器:

docker exec -it fabric-cli bash

1. 在org1中安装 | 运行以下链码安装:

. scripts/set-env.sh 1 0 7051
peer lifecycle chaincode install chaincode/credit_chaincode_plus.tar.gz

 

2. 在org2中安装
运行以下链码安装:

. scripts/set-env.sh 2 0 9051
peer lifecycle chaincode install chaincode/credit_chaincode_plus.tar.gz

 

3. 查看安装情况

peer lifecycle chaincode queryinstalled

 

三.批准链码
org1批准链码

设置链码环境变量

export CC_PACKAGE_ID=credit_chaincode_plus_1.1:a7d65de21f706c25029b84cbbf7de2163d06d9b8e9fc33fd20a5a59cc3a18b01

2. 设置Org1环境变量

. scripts/set-env.sh 1 0 7051

3.批准链码

peer lifecycle chaincode approveformyorg -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com --channelID $CHANNEL_NAME --name credit_chaincode_plus --version 1.1 --package-id $CC_PACKAGE_ID --sequence 2 --tls --cafile $ORDERER_CA

 

1. 设置Org2环境变量

. scripts/set-env.sh 2 0 9051

2.批准链码

peer lifecycle chaincode approveformyorg -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com --channelID $CHANNEL_NAME --name credit_chaincode_plus --version 1.1 --package-id $CC_PACKAGE_ID --sequence 2 --tls --cafile $ORDERER_CA

 

检查提交准备

peer lifecycle chaincode checkcommitreadiness --channelID $CHANNEL_NAME --name credit_chaincode_plus --version 1.1 --sequence 2 --tls --cafile $ORDERER_CA --output json

 

提交链码

peer lifecycle chaincode commit -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com --channelID $CHANNEL_NAME --name credit_chaincode_plus --version 1.1 --sequence 2 --tls --cafile $ORDERER_CA --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles $PEER0_ORG1_CA --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles $PEER0_ORG2_CA

 

查询提交的链码

peer lifecycle chaincode querycommitted --channelID $CHANNEL_NAME --name credit_chaincode_plus --tls --cafile $ORDERER_CA

 

查看运行镜像形成容器情况

docker logs -f b1ddd21303a5

 

调用createCreditSubject功能

peer chaincode invoke -o orderer.example.com:7050 --tls --cafile $ORDERER_CA --channelID $CHANNEL_NAME --name credit_chaincode_plus --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles $PEER0_ORG1_CA --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles $PEER0_ORG2_CA -c '{"function":"createCreditSubjectPlus", "Args":["A001", "My Company", "Company","Admin","2024-05-28 14:35:00"]}'

调用queryCreditSubject功能

peer chaincode query -C $CHANNEL_NAME --name credit_chaincode_plus -c '{"function":"queryCreditSubjectPlus","Args":["A001"]}'

调用deleteCreditSubject功能

peer chaincode invoke -o orderer.example.com:7050 --tls --cafile $ORDERER_CA --channelID $CHANNEL_NAME --name credit_chaincode_plus --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles $PEER0_ORG1_CA --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles $PEER0_ORG2_CA -c '{"function":"deleteCreditSubject", "Args":["A001"]}'

 

调用updateSubjectScore功能(这边如果嫌麻烦的话可以更新功能之后再调用删除功能)

peer chaincode invoke -o orderer.example.com:7050 --tls --cafile $ORDERER_CA --channelID $CHANNEL_NAME --name credit_chaincode_plus --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles $PEER0_ORG1_CA --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles $PEER0_ORG2_CA -c '{"function":"updateSubjectScore", "Args":["A002","10"]}'

 

 


 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/656028.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Debug-012-el-popover 使用 doClose() 关闭窗口不生效的处理方案

前言: 今天上午碰见一个非常奇怪的情况:一样的方法实现的功能,效果却不一样。 两个页面都是使用的doClose()去关闭的el-popover,其中有一个就是不生效,找不同找了半天,始终不得其解。请看效果吧&#xff1…

百度页面奔跑的白熊html、css

一、相关知识-动画 1.基本使用:先定义再调用 2. 调用动画 用keyframes定义动画(类似定义类选择器) keyframes动画名称{ 0%{ width:100px; } 100%{ width:200px; } } 使用动画 div { width:200px; height:200px; background-…

从华为云Redis到AWS ElastiCache的操作方法

越来越多企业选择出海,那么就涉及到IT系统的迁移,本文将详细介绍如何将华为云Redis顺利迁移到AWS ElastiCache的操作方法,九河云将为您介绍迁移步骤以帮助您顺利完成这一重要任务。 **1. 确定迁移计划** 在开始迁移之前,首先要制…

身为UI设计老鸟,不学点3D,好像要被潮流抛弃啦,卷起来吧。

当前3D原则在UI设计中运用的越来越多,在UI设计中,使用3D元素可以为界面带来以下几个价值: 增强视觉冲击力:3D元素可以通过立体感和逼真的效果,为界面增添视觉冲击力,使得设计更加生动、吸引人,并…

在VS Code中进行Java的单元测试

在VS Code中可以使用 Test Runner for Java扩展进行Java的测试执行和调试。 Test Runner for Java的功能 Test Runner for Java 结合 Language Support for Java by Red Hat 和 Debugger for Java这两个插件提供如下功能: 运行测试: Test Runner for …

protobuf —— 快速上手

protobuf —— 快速上手 创建 .proto 文件添加注释指定proto3语法package 声明符定义消息(message) 定义消息字段字段定义基本格式字段名称命名规范字段类型字段唯一编号示例 转换关系示例:增加姓名和年龄字段 字段唯一编号字段编号范围编码效…

短视频真人配音:成都科成博通文化传媒公司

短视频真人配音:情感传递的新维度 随着数字化媒体的飞速发展,短视频已经成为人们日常生活中不可或缺的一部分。而在这个视觉盛宴的时代,真人配音的加入为短视频注入了新的活力,不仅丰富了内容形式,更使得情感传递达到…

Oracle EBS API创建AP发票报错:ZX_TAX_STATUS_NOT_EFFECTIVE和ZX_REGIME_NOT_EFF_IN_SUBSCR-

背景 由创建国外业务实体财务未能提供具体国家地区会计税制,而是实施人员随便选择其它国外国家地区会计税制。导致客户化创建AP发票程序报错:UNEXPECTED TAX ERROR-导入时出现意外的税务错误ZX_TAX_STATUS_NOT_EFFECTIVE-ZX_REGIME_NOT_EFF_IN_SUBSCR-ZX…

基于双向长短期记忆BiLSTM对消费者投诉进行多类分类

前言 系列专栏:【深度学习:算法项目实战】✨︎ 涉及医疗健康、财经金融、商业零售、食品饮料、运动健身、交通运输、环境科学、社交媒体以及文本和图像处理等诸多领域,讨论了各种复杂的深度神经网络思想,如卷积神经网络、循环神经网络、生成对抗网络、门控循环单元、长短期记…

ssm150旅游网站的设计与实现+jsp

旅游网站设计与实现 摘 要 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本旅游网站就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞…

STM32学习和实践笔记(30):窗口看门狗(WWDG)实验

1.WWDG介绍 1.1 WWDG简介 上一章我们已经介绍了IWDG,知道它的工作原理就是一个12位递减计数器不断递减计数,当减到0之前还未进行喂狗的话,产生一个MCU复位。 窗口看门狗WWDG其实和独立看门狗类似,它是一个7位递减计数器不断的往…

学至乎没而后止也

开场白 学至后没而后止也这个题目的原话来自与荀子《劝学》。大家知道什么意思吗?学习要学到你人没了,才算停止了。通俗点说就是只要没学死就往死里学,高雅点说就是要保持终身学习。 在以前说终身学习好像是一种良好习惯或品德,…

Android NDK系列(一)手动搭建Native Project

使用NDK编写的本地代码具有高性能等特性,在游戏、图形处理等领域有广泛应用,下面介绍如何手动搭建一个纯C版的Android项目,通过该项目可以理解Android的项目结构。 一、创建settings.gradle Android项目是基于Gradle构建的,首先得…

【go项目01_学习记录15】

重构MVC 1 Article 模型1.1 首先创建 Article 模型文件1.2 接下来创建获取文章的方法1.3 新增 types.StringToUint64()函数1.4 修改控制器的调用1.5 重构 route 包1.6 通过 SetRoute 来传参对象变量1.7 新增方法:1.8 控制器将 Int64ToString 改为 Uint64ToString1.9…

ubuntu24.04LVM扩容问题

目录 一、 开机前设置:扩展 二、 开机后设置:分区管理 通过gparted管理分区有效做法。 一、 开机前设置:扩展 虚拟机关机。打开虚拟机设置。 挂起状态是不能扩容的 这里选择扩容到40G 二、 开机后设置:分区管理 使用gpar…

基于Matlab的车道线检测系统 (文末有代码获取链接)【含Matlab源码 MX_001期】

运行环境:Matlab2014b 部分代码: %% 视频流循环处理 % 创建一个循环过程来对给定视频进行车道线检测 % 该循环使用之前初始化的系统对象 warningTextColors {[1 0 0], [1 0 0], [0 0 0], [0 0 0]}; while ~isDone(hVideoSrc) RGB step(hVideoSrc);% …

Java入门基础学习笔记43——包

什么是包? 包是用来分门别类的管理各种不同程序的,类似文件夹,建包有利于程序的管理和维护。 建包的语法规则: package cn.ensource.javabean;public class Car() {} 在自己的程序中调用其他包下的程序的注意事项: 1…

Web3探索加密世界:空投常见类型有哪些?附操作教程

每种空投类型都有独特的特征和目的,我们需要了解不同类型的加密空投。本文给大家介绍的是流行的加密货币空投类型,以及一般空投是如何做的。感兴趣的话请看下去。 一、空投常见类型 1、持有者空投 持有者空投向钱包中持有一定数量数字货币的人免费发放…

探索Python的包与模块:构建项目的基石

新书上架~👇全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 目录 一、模块与包的基础认知 1. 模块的定义与创建 2. 包的组织与管理 二、模块与包的进阶使用…

【测评】香橙派 AIpro上手初体验

AI毋庸置疑是近年来,热度最高的技术之一,作为一名工程师拥抱新技术的同时不可或缺的需要一块强悍的开发板,香橙派 AIpro除了拥有好看的皮囊之外,还拥有一个有趣且充满魅力的灵魂。作为一位长期活跃在嵌入式开发领域的工程师&#…