微信小程序 对接蓝牙设备连接实现打印
微信小程序
微信小程序对接蓝牙设备连接
初始化蓝牙模块 wx.openBluetoothAdapter()
初始化完成后搜寻附近的蓝牙设备 wx.startBluetoothDevicesDiscovery()
监听寻找到新设备的事件 wx.onBluetoothDeviceFound()
在监听寻找到新设备的事件回调中获取所有蓝牙设备列表 wx.getBluetoothDevices()
连接低功耗蓝牙设备 wx.createBLEConnection()
连接成功后获取蓝牙设备服务 wx.getBLEDeviceServices()
在服务中取(notify=true || indicate=true) && write=true 的特征值的 uuid: wx.getBLEDeviceCharacteristics()
完成后停止搜寻 wx.stopBluetoothDevicesDiscovery()
向低功耗蓝牙设备特征值中写入二进制数据 wx.writeBLECharacteristicValue()
离开页面时取消蓝牙连接 wx.closeBLEConnection()
关闭蓝牙模块 wx.closeBluetoothAdapter()
获取mac地址
在使用蓝牙的过程中,我们需要获取蓝牙设备的Mac地址。在Android设备上,onBluetoothDeviceFound方法中,deviceId是蓝牙设备的Mac地址。而在 iOS设备上,deviceId则是蓝牙设备的uuid。我们想要在iOS设备上获取Mac地址,就需要自己想办法。
https://blog.csdn.net/qq_25430563/article/details/124557109
services 180A
{
"errno":0,
"deviceId":"17512E41-549B-037C-4B22-E46A0FF3BA45",
"services":[
{ "isPrimary":true, "uuid":"49535343-FE7D-4AE5-8FA9-9FAFD205E455" },
{ "isPrimary":true, "uuid":"E7810A71-73AE-499D-8C15-FAA9AEF0C3F2" },
{ "isPrimary":true, "uuid":"000018F0-0000-1000-8000-00805F9B34FB" },
{ "isPrimary":true, "uuid":"0000180A-0000-1000-8000-00805F9B34FB" }
],
"errMsg":"getBLEDeviceServices:ok", "errCode":0
}
getBLEDeviceCharacteristics 日志
{ "characteristics":[
{
"properties":{
"writeDefault":false,
"notify":true,
"write":false,
"indicate":false,
"read":false,
"writeNoResponse":false
},
"uuid":"49535343-1E4D-4BD9-BA61-23C647249616"
},
{
"properties":{
"writeDefault":true,
"notify":false,
"write":true,
"indicate":false,
"read":false,
"writeNoResponse":true
},
"uuid":"49535343-8841-43F4-A8D4-ECBE34729BB3"
}
],
"deviceId":"17512E41-549B-037C-4B22-E46A0FF3BA45",
"serviceId":"49535343-FE7D-4AE5-8FA9-9FAFD205E455",
"errno":0, "errMsg":"getBLEDeviceCharacteristics:ok", "errCode":0
}
代码 蓝牙搜索
// index.js
// 获取应用实例
const app = getApp()
const PrinterJobs = require('../../printer/printerjobs')
const printerUtil = require('../../printer/printerutil')
Page({
data: {
deviceName: 'aaa',
bleInputValue: 'Qsprinter', //P8J12204110019\r\n
deviceId: '',
bleDeviceList: [],
serviceId: '',
notifyId: '',//getCharacteId()
writeId: '',
},
// 事件处理函数
bindViewTap() {
wx.navigateTo({
url: '../logs/logs'
})
},
onLoad() {
//this.getAddressData()
},
//-------------------ble--------------------------------
showToast(msg){
wx.showToast({ title: msg, })
},
initBluetooth:function(){
wx.openBluetoothAdapter({
//mode: '',
success: (res) =>{
this.findBlueList();//2.0
},
fail: (res) =>{
//如果手机上的蓝牙没有打开,可以提醒用户
this.showToast('手机蓝牙没打开')
console.log(res)
}
})
},
findBlueList: function(){
wx.showLoading({ title: '搜索附近设备', })
wx.startBluetoothDevicesDiscovery({ //该接口非常耗电
//services: ['FEE7'],
allowDuplicatesKey: false,
interval: 0,
success:(res) =>{
console.log('discovery, 搜索所有设备: %s', JSON.stringify(res))
this.getBlue()
},
fail:(res) =>{
//wx.hideLoading()
console.log('discovery, 搜索附近的蓝牙失败')
}
})
},
closeBlue: function(){
wx.stopBluetoothDevicesDiscovery({ // 连接蓝牙成功之后关闭蓝牙搜索
success: (res) => {
console.log('close 关闭蓝牙搜索') //this.showToast('关闭蓝牙搜索')
}
})
},
getBlue: function(){
wx.getBluetoothDevices({
success: (res) => {
//wx.hideLoading();
console.log('devices, 搜到设备总数: %d, 打印列表: \n%s', res.devices.length, JSON.stringify(res.devices))
//如果搜所到设备列表为空
if(res.devices.length == 0){
// 监听搜索到新设备
wx.onBluetoothDeviceFound((bres) => {
for(var i = 0; i < bres.devices.length; i++){
let d = bres.devices[i]
if(d.name=='' || d.name.startsWith('P8') || d.name.startsWith("未知设备") || d.name.startsWith('Xiao')){
continue //过滤设备
}
if(d.name == this.data.bleInputValue || d.localName == this.data.bleInputValue){
console.log('devs size 0, getBluetooth, name:' + d.name)
this.getBlue()
}
}
})
console.log('devs 0,-----------end')
return
}else{
console.log('devs list, find ok -----------' + res.devices.length); //console.log(res.devices)
this.setData({ bleDeviceList: res.devices })
for(var i = 0; i < res.devices.length; i++){
//判断列表中是否包含我的蓝牙设备
var d = res.devices[i];
if(d.name == this.data.bleInputValue){
this.setData({ deviceId: d.deviceId })
this.connectBlue(d)
return
}
}
}
},
fail: () => {
console.log("devs 搜索蓝牙设备失败")
}
})
},
connectBlue(dev){
var deviceId = this.data.deviceId
var deviceName = dev.name + " " + deviceId;
console.log("connect, 连接设备 name: %s, data: %s", deviceName, JSON.stringify(dev))
this.showToast("正在连接: " + deviceName)
//
//this.createBLEConnection_test()
//
wx.createBLEConnection({
deviceId: this.data.deviceId,
success: (res) =>{
console.log('connect, 蓝牙连接成功!' + deviceName)
this.showToast("连接成功:" + deviceName)
this.closeBlue()
this.getServiceId()//5.0
},
fail: () => {
this.showToast("连接失败: " + deviceName)
console.log('connect, 连接失败 ' + deviceName)
},
complete: () =>{
//wx.hideLoading();
}
})
},
getServiceId(){
// 5 连接上需要的蓝牙设备之后,获取蓝牙设备的服务uuid
console.log('获取蓝牙设备的serviceId')
wx.getBLEDeviceServices({
deviceId: this.data.deviceId,
success: (res) => {
console.log('serviceId, 列表 res: %s', JSON.stringify(res))
var model = res.services[0]
this.setData({ serviceId: model.uuid })
console.log('serviceId, uuid: ' + model.uuid)
this.getCharacteId()
// // 使用包含 180A 的服务id
// if (model.uuid.indexOf('180A')) {
// console.log('serviceId, 包含 180A 的服务id, uuid: ' + model.uuid)
// this.setData({ serviceId: model.uuid })
// this.getCharacteId() //6.0
// }
}
})
},
/**
* 6.获取蓝牙设备某个服务中所有特征值(characteristic)
* 如果一个蓝牙设备需要进行数据的写入以及数据传输,就必须具有某些特征值,所以通过上面步骤获取的id可以查看当前蓝牙设备的特征值
* characteristic
* uuid:蓝牙设备特征值的 uuid
* properties:该特征值支持的操作类型
*/
getCharacteId(){
wx.getBLEDeviceCharacteristics({
deviceId: this.data.deviceId,
serviceId: this.data.serviceId,
success: (res) =>{
console.log('characteId, getBLEDeviceCharacteristics: ', JSON.stringify(res))
for(let i = 0; i < res.characteristics.length; i++){ //2个特征值
var model = res.characteristics[i]
if(model.properties.notify){
this.setData({ notifyId: model.uuid }) //监听的值
console.log('characteId, notifyId 获取监听的值 , uuid: %s', model.uuid)
this.startNotice(model.uuid)
}
if(model.properties.write){
console.log('writeId, uuid: %s', model.uuid)
this.setData({ writeId: model.uuid }) //用来写入的值
}
使用含有 2A23 对应的特征值
// if (!model.uuid.indexOf('2A23')) {
// console.log('characteId, failed 找不到含有 2A23 对应的特征值,: %s', model.uuid)
// }
// if (model.uuid.indexOf('2A23')) {
// if(model.properties.notify == true){
// this.setData({notifyId: model.uuid }) //监听的值
// console.log('characteId, 使用含有 2A23 对应的特征值, 监听的值 characterId: %s', model.uuid)
// this.startNotice(model.uuid)
// }
// if(model.properties.write == true){
// console.log('writeId, uuid: %s', model.uuid)
// this.setData({ writeId: model.uuid }) //用来写入的值
// }
// }
wx.readBLECharacteristicValue({
deviceId: this.data.deviceId,
serviceId: this.data.serviceId,
characteristicId: this.data.notifyId,
success: res => {
console.log('mac, readBLECharacteristicValue ----- res: %s', JSON.stringify(res))
}
})
}
}
})
},
startNotice(uuid){
console.log('startNotice uuid--------', uuid)
wx.notifyBLECharacteristicValueChange({ //7.创建连接,发送指令
state: true, // 启用 notify 功能
deviceId: this.data.deviceId,
serviceId: this.data.serviceId,
characteristicId: uuid, //第一步 开启监听 notityid 第二步发送指令 write
success: (res) => {
console.log('write, notifyBLECharacteristicValueChange --- res: %s', JSON.stringify(res))
// // 当特征值是查询 Mac 地址时 add
// if (res.characteristicId.indexOf('2A23')) {
// let macInfo = (array2String(res.value)).toUpperCase()
// console.log('mac info ---2A23--------', macInfo)
// }
console.log('========== send start==========')
this.sendPrinterJobsCommand();
console.log('========== send end==========')
//
//设备返回的方法 // 监听低功耗蓝牙连接的错误事件
wx.onBLECharacteristicValueChange((result) => {
//此时可以拿到蓝牙设备返回来的数据是一个ArrayBuffer类型数据,所以需要通过一个方法转换成字符串
var nonceId = this.ab2hex(result.value)
console.log('onBLECharacteristicValueChange, get nonceId: %s, result: %s', nonceId, JSON.stringify(result))
拿到这个值后,要去后台请求服务(当前步骤根据当前需求自己书写),获取下一步操作指令写入到蓝牙设备上去
// wx.request({
// url: url,
// method: 'POST',
// data:{ describe: nonceId },
// success: (result2) =>{
// console.log('request, result2: %s', JSON.stringify(result2))
// //res.data.data.ciphertext; 我这边的服务返回来的是16进制字符串,蓝牙设备是接收不到当前格式的数据,需要转换成ArrayBuffer
// this.onSendCommand(this.string2buffer(result2.data.data.ciphertext)) //8.0
// //服务器返回一个命令,我们要把这个命令写入蓝牙设备
// }
// })
})
}
})
},
onSendCommand(buffer){
wx.writeBLECharacteristicValue({
deviceId: this.data.deviceId,
serviceId: this.data.serviceId,
characteristicId: this.data.writeId,//第二步写入的特征值
value: buffer, // 这里的value是ArrayBuffer类型
success: (res) => {
console.log('write, 写入成功 send ok, res: %s', JSON.stringify(res))
},
fail: () => {
console.log('写入失败')
},
complete:() => {
console.log("调用结束");
}
})
},
//下面是需要使用到的两个格式相互转换的方法
// 将字符串转换成ArrayBufer
string2buffer(str) {
console.log("将字符串转换成ArrayBufer-----" + str)
let val = ""
if(!str){
return;
}
let length = str.length;
let index = 0;
let array = []
while(index < length){
array.push(str.substring(index, index+2));
index = index + 2;
}
val = array.join(",");
// 将16进制转化为ArrayBuffer
return new Uint8Array(val.match(/[\da-f]{2}/gi).map(function (h) {
return parseInt(h, 16)
})).buffer
},
//
str2ArrayBuff(str) {
// Convert str to ArrayBuff and write to printer
let buffer = new ArrayBuffer(str.length)
let dataView = new DataView(buffer)
for (let i = 0; i < str.length; i++) {
dataView.setUint8(i, str.charAt(i).charCodeAt(0))
}
return buffer;
},
// 将ArrayBuffer转换成字符串
ab2hex(buffer) {
console.log("将ArrayBuffer转换成字符串")
var hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
function (bit) {
return ('00' + bit.toString(16)).slice(-2)
}
)
return hexArr.join('');
},
//
array2String(buffer) {
let hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
function (bit) {
return ('00' + bit.toString(16)).slice(-2)
}
)
return `${hexArr[7]}:${hexArr[6]}:${hexArr[5]}:${hexArr[2]}:${hexArr[1]}:${hexArr[0]}`
},
sendPrinterJobsCommand(){
let printerJobs = new PrinterJobs();
printerJobs
.print('2022年12月5日17:34')
.print(printerUtil.fillLine())
.setAlign('ct')
.setSize(2, 2)
.println('深圳市公安局交通警察支队南山大队\n违规停放机动车提示单')
.setSize(1, 1)
.print('切尔西Chelsea')
.setSize(2, 2)
.print('在线支付(已支付)')
.setSize(1, 1)
.print('订单号:5415221202244734')
.print('下单时间:2022-12-05 18:08:08')
.setAlign('lt')
.print(printerUtil.fillAround('一号口袋'))
.print(printerUtil.inline('意大利茄汁一面 * 1', '15'))
.print(printerUtil.fillAround('其他'))
.print('餐盒费:1')
.print('[赠送康师傅冰红茶] * 1')
.print(printerUtil.fillLine())
.setAlign('rt')
.print('原价:¥16')
.print('总价:¥16')
.setAlign('lt')
.print(printerUtil.fillLine())
.print('备注')
.print("无")
.print(printerUtil.fillLine())
.println();
//
let buffer = printerJobs.buffer();
console.log('ArrayBuffer', 'length: ' + buffer.byteLength, ' hex: ' + this.ab2hex(buffer));
//this.onSendCommand(buffer);
//
},
/**
* 源代码 小票打印案例 参考
*/
writeBLECharacteristicValue() {
let printerJobs = new PrinterJobs();
printerJobs
.print('2018年12月5日17:34')
.print(printerUtil.fillLine())
.setAlign('ct')
.setSize(2, 2)
.println('#20饿了么外卖')
.setSize(1, 1)
.print('切尔西Chelsea')
.setSize(2, 2)
.print('在线支付(已支付)')
.setSize(1, 1)
.print('订单号:5415221202244734')
.print('下单时间:2017-07-07 18:08:08')
.setAlign('lt')
.print(printerUtil.fillAround('一号口袋'))
.print(printerUtil.inline('意大利茄汁一面 * 1', '15'))
.print(printerUtil.fillAround('其他'))
.print('餐盒费:1')
.print('[赠送康师傅冰红茶] * 1')
.print(printerUtil.fillLine())
.setAlign('rt')
.print('原价:¥16')
.print('总价:¥16')
.setAlign('lt')
.print(printerUtil.fillLine())
.print('备注')
.print("无")
.print(printerUtil.fillLine())
.println();
let buffer = printerJobs.buffer();
console.log('ArrayBuffer', 'length: ' + buffer.byteLength, ' hex: ' + this.ab2hex(buffer));
// 1.并行调用多次会存在写失败的可能性
// 2.建议每次写入不超过20字节
// 分包处理,延时调用
const maxChunk = 20;
const delay = 20;
for (let i = 0, j = 0, length = buffer.byteLength; i < length; i += maxChunk, j++) {
let subPackage = buffer.slice(i, i + maxChunk <= length ? (i + maxChunk) : length);
setTimeout(this._writeBLECharacteristicValue, j * delay, subPackage);
}
},
/**
*
* @param {*} text
*/
printImageQrcode(text){
//
},
//绘制二维码
draw() {
//设置text为二维码码值,一般通过接口获取后赋值
let text = '123123' + Math.random(0,100);
//设置倒计时为5秒 this.setData({ countDown: 5, })
const query = wx.createSelectorQuery()
let that = this
query.select('#myQrcode')
.fields({
node: true,
size: true
})
.exec(async (res) => {
let canvas = res[0].node
var img = canvas.createImage();
//二维码中间那个小logo的地址
img.src = 'https://profile.csdnimg.cn/D/5/5/1_luo1831251387'
img.onload = function () {
// img.onload完成后才能调用 drawQrcode方法
var options = {
canvas: canvas,
canvasId: 'myQrcode',
width: 350,
padding: 30,
paddingColor: '#fff',
background: '#fff',
foreground:'rgb(0, 178, 106)' ,
//值
text: text,
image: {
imageResource: img,
width: 70, // logo图片大小 建议不要设置过大,以免影响扫码
height: 70, // 建议不要设置过大,以免影响扫码
round: true // Logo图片是否为圆形
}
}
drawQrcode(options)
// 获取临时路径(得到之后,想干嘛就干嘛了)
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: 350,
height: 350,
destWidth: 600,
destHeight: 600,
canvasId: 'myQrcode',
canvas: canvas,
success(res) {
console.log('二维码临时路径为:', res.tempFilePath)
},
fail(res) {
console.error(res)
}
})
};
})
},
//打印二维码
printImgT(device, text) {//小票打印
device = { "deviceId": this.data.deviceId, "serviceId": this.data.serviceId, "writeId": this.data.writeId }
let tthis = this;
const ctx = wx.createCanvasContext('canvas');
ctx.clearRect(0, 0, 160, 160);
drawQrcode({
canvasId: 'canvas',
text: String(text),
width: 100,
height: 100,
callback(e) {
setTimeout(() => {
// 获取图片数据
wx.canvasGetImageData({
canvasId: 'canvas',
x: 0,
y: 0,
width: 160,
height: 160,
success(res) {
let arr = tthis.convert4to1(res.data);
let data = tthis.convert8to1(arr);
const cmds = [].concat([27, 97, 1], [29, 118, 48, 0, 20, 0, 160, 0], data, [27, 74, 3], [27, 64]);
const buffer = toArrayBuffer(Buffer.from(cmds, 'gb2312'));
let arrPrint = [];
arrPrint.push(util.sendDirective([0x1B, 0x40]));
// arrPrint.push(util.sendDirective([0x1B, 0x61, 0x01])); //居中
for (let i = 0; i < buffer.byteLength; i = i + 20) {
arrPrint.push(buffer.slice(i, i + 20));
}
arrPrint.push(util.hexStringToBuff("\n"));
arrPrint.push(util.sendDirective([0x1B, 0x61, 0x01])); //居中
arrPrint.push(util.hexStringToBuff("扫码识别订单号\n"));
arrPrint.push(util.hexStringToBuff("\n"));
arrPrint.push(util.hexStringToBuff("\n"));
tthis.printInfo(device, arrPrint);
}
})
}, 3000);
}
});
},
printInfo: function(device, arr, callback) {
let tthis = this;
if (arr.length > 0) {
tthis.sendStr(device, arr[0], function(success) {
arr.shift();
tthis.printInfo(device, arr, callback);
}, function(error) {
console.log(error);
});
} else {
callback ? callback() : '';
}
},
//发送数据
sendStr: function(device, bufferstr, success, fail) {
let tthis = this;
console.log('sendStr', device);
wx.writeBLECharacteristicValue({
deviceId: device.deviceId,
serviceId: device.serviceId,
characteristicId: device.characteristicId,
value: bufferstr,
success: function(res) {
success(res);
console.log('sendStr', bufferstr)
},
failed: function(res) {
fail(res)
console.log("数据发送失败:" + JSON.stringify(res))
},
complete: function(res) {
console.log("发送完成:" + JSON.stringify(res))
}
})
},
//4合1
convert4to1(res) {
let arr = [];
for (let i = 0; i < res.length; i++) {
if (i % 4 == 0) {
let rule = 0.29900 * res[i] + 0.58700 * res[i + 1] + 0.11400 * res[i + 2];
if (rule > 200) {
res[i] = 0;
} else {
res[i] = 1;
}
arr.push(res[i]);
}
}
return arr;
},
//8合1
convert8to1(arr) {
let data = [];
for (let k = 0; k < arr.length; k += 8) {
let temp = arr[k] * 128 + arr[k + 1] * 64 + arr[k + 2] * 32 + arr[k + 3] * 16 + arr[k + 4] * 8 + arr[k + 5] * 4 +
arr[k + 6] * 2 + arr[k + 7] * 1
data.push(temp);
}
return data;
},
})
旧的版本 wx.createCanvasContext()
weapp.qrcode
微信小程序二维码生成工具 weapp.qrcode
https://blog.goodsunlc.com/archives/376.html
https://juejin.cn/post/6844903559251640328
https://www.cnblogs.com/huangguofeng/p/13735859.html
代码 printImgT()方法
wx-bluetooth
https://gitee.com/copperpeas/wx-bluetooth/blob/master/pages/index/index.js
PrinterJobs 工具下载
https://github.com/benioZhang/miniprogram-bluetoothprinter/tree/master/printer
扫普通链接二维码打开小程序
扫普通链接二维码打开小程序
https://developers.weixin.qq.com/miniprogram/introduction/qrcode.html#%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D
蓝牙
企业微信 蓝牙
https://developer.work.weixin.qq.com/document/path/90771
微信小程序对接蓝牙设备连接
https://blog.csdn.net/qq_43384836/article/details/121632237
https://blog.csdn.net/weixin_34200628/article/details/91391230?utm_medium=distribute.pc_relevant.none-task-blog-title-3&spm=1001.2101.3001.4242
Canvas 2D 绘制二维码
下载 weapp-qrcode-canvas-2d 示例代码(官网)
画布。2.9.0 起支持一套新 Canvas 2D 接口(需指定 type 属性),同时支持同层渲染
https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html#Canvas-2D-%E7%A4%BA%E4%BE%8B%E4%BB%A3%E7%A0%81
二维码生成工具 weapp-qrcode-canvas-2d
gitee仓库地址 https://gitee.com/WeiDoctor/weapp-qrcode-canvas-2d
教程
https://developers.weixin.qq.com/community/develop/article/doc/000e88e73a415835ed9b46d7956013
案例2
微信小程序绘制二维码
https://blog.csdn.net/luo1831251387/article/details/125641264
https://blog.csdn.net/weixin_46899191/article/details/122237849
https://m.php.cn/article/487088.html
createSelectorQuery
getImageData 获取像素点数据
用canvas中的getImageData方法获取像素点数据
https://blog.csdn.net/qq_40129176/article/details/103600918
node null
创建’2d’canvas出现Cannot read ‘node’ of null的问题?
https://developers.weixin.qq.com/community/develop/doc/000caea9508b18e7c5f9581f258c00
热敏打印机编程 ESC/POS指令
https://www.jianshu.com/p/dd6ca0054298
小程序 canvas2d
https://blog.csdn.net/weixin_39685861/article/details/106033494
打印相关链接
### 打印二维码(old)
微信小程序蓝牙打印二维码 https://blog.csdn.net/cn_zgt_boss/article/details/104068404
wx.createCanvasContext方法已废弃的解决方案 https://blog.csdn.net/zs1028/article/details/124788038
微信小程序连接蓝牙打印小票
https://blog.csdn.net/LDL_CQ/article/details/122835290
微信小程序蓝牙对接热敏打印机
https://blog.csdn.net/qq_37970097/article/details/119148707
打印图片
微信小程序连接蓝牙打印机打印图片 https://blog.csdn.net/cn_zgt_boss/article/details/104068404
用微信小程序打印取件码标签 http://56diandian.com/wcx-dyqjm/
微信小程序实现蓝牙打印(图片、二维码、文字)
https://blog.csdn.net/sghu8023/article/details/116234704
小程序实现BLE蓝牙连接硬件设备
https://www.jianshu.com/p/0f4ff8a2249d
参考资料
微信官方文档 蓝牙
https://developers.weixin.qq.com/miniprogram/dev/framework/device/ble.html
微信小程序连接蓝牙设备并传递数据
https://blog.csdn.net/qq_41889034/article/details/123981966
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)