全部文章

<h1>前置</h1> <ol> <li>票据打印机(支持 ESC/POS 指令),需要内置蜂鸣器</li> <li>打印机连接到电脑</li> <li>Windows 11 系统</li> <li>使用 zadig 修改默认驱动。为什么要改默认驱动?<a href="https://github.com/libusb/libusb/wiki/Windows">看这里</a></li> <li>需要提前了解 usb 的 <code>vendor id</code>、<code>product id</code>、<code>config</code>、<code>endpoint</code>、<code>interface number</code></li> <li>需要提前了解 usb 通信过程。</li> </ol> <p><img src="/uploads/tauri-printer-sys.svg" alt="image.svg" /></p> <h1>框图</h1> <p><img src="/uploads/tauri-printer-flow.png" alt="" /></p> <h1>UI</h1> <p><img src="/uploads/tauri-printer.png" alt="image.png" /></p> <h1>流程</h1> <ol> <li>前端页面加载完毕后,请求获取 usb 设备列表。</li> </ol> <pre><code class="language-tsx">useEffect(() => { getUsbDevices(); }, []); async function getUsbDevices() { invoke<UsbDevice[]>("get_usb_devices") .then((result) => { setUsbDevices(result); }) .catch((error) => { console.error("get usb list err", error); }); }</code></pre> <ol start="2"> <li>后端(Rust 端)初始化 usb 上下文并获取 usb 设备列表,然后根据 <code>vendor_id</code> 过滤出我们需要的 usb 列表,然后将 usb 设备的 <code>vendor_id</code>, <code>product_id</code>, <code>manufacturer_name</code>, <code>product_name</code> 等信息以 json 格式返回给前端。</li> </ol> <pre><code class="language-rs">#[tauri::command] async fn get_usb_devices() -> Vec<UsbDevice> { let context = libusb::Context::new().unwrap(); let mut usb_devices: Vec<UsbDevice> = Vec::new(); for device in context.devices().unwrap().iter() { if let Ok(d1) = read_usb_device(device) { usb_devices.push(d1); } } return usb_devices; } fn read_usb_device(device: Device) -> Result<UsbDevice, String> { let device_desc = device.device_descriptor().unwrap(); // 根据 vendor_id 过滤 if device_desc.vendor_id() != 0x1FC9 { return Err("not valid printer".into()); } // 构造返回给前端的 usb device 信息 let mut usb_device = UsbDevice { bus_number: device.bus_number(), address: device.address(), vendor_id: device_desc.vendor_id(), product_id: device_desc.product_id(), manufacturer_name: Some(String::from("unknow manufacturer name")), product_name: Some(String::from("unknow product name")), serial_number: Some(String::from("unknow serial number")), }; // 为了获取 usb 设备名称,或许有更好的方式获取,如果你知道,请记得给我留言。 match device.open() { Ok(handle) => { let timeout = Duration::from_secs(60); if let Ok(languages) = handle.read_languages(timeout) { if languages.len() > 0 { let language = languages[0]; usb_device.manufacturer_name = handle .read_manufacturer_string(language, &device_desc, timeout) .ok(); usb_device.product_name = handle .read_product_string(language, &device_desc, timeout) .ok(); usb_device.serial_number = handle .read_serial_number_string(language, &device_desc, timeout) .ok(); } } } Err(e) => { println!("can not open device: {:?}", e); // return Err("can not open device".into()); } } return Ok(usb_device); }</code></pre> <ol start="3"> <li>用户从 usb 设备列表下拉框中选中一个 usb 设备,点击“蜂鸣器”,此时前端会把 <code>vendor_id</code>, <code>product_id</code> 和使蜂鸣器响的指令(以<code>“27, 66, 2, 2”</code> 字符串的形式)这是 <code>ESC/POS</code> 指令,传给后端。</li> </ol> <pre><code class="language-tsx">const handleUsbBeep = async () => { const { vendor_id: vid, product_id: pid } = usbDevice || {}; try { const cmd = [27, 66, 2, 2].join(","); await invoke<string>("write_to_device", { pid, vid, cmdStr: cmd }); } catch (err: any) { message.error(err); } };</code></pre> <ol start="4"> <li>后端根据接收到 <code>vendor_id</code> 和 <code>product_id</code> 打开设备,然后设置激活的配置(调用DeviceHandle 上的 <code>set_active_configuration(1)</code>)和声明接口(调用 DeviceHandle 上的 <code>claim_interface(0)</code>)1 和 0,我这里是写死的,不同的 usb 设备,这两个值或许会不一样。</li> </ol> <pre><code class="language-rs">#[tauri::command] async fn write_to_device(pid: u16, vid: u16, cmd_str: &str) -> Result<String, String> { println!("received: {:04x}:{:04x}, {:?}", pid, vid, cmd_str); let context = libusb::Context::new().unwrap(); let usb_device = context.open_device_with_vid_pid(vid, pid); match usb_device { Some(b) => write(b, cmd_str), _ => panic!("can not open device"), } }</code></pre> <ol start="5"> <li>转换字符串命令为 <code>Vec<u8></code> 形式,然后通过 <code>write_bulk(endpoint, buf, timeout)</code> 方法发送到 usb 设备。注意 <code>endpoint</code> 我的这里写死的是 <code>3</code>,这个要根据 usb 设备来变。</li> </ol> <pre><code class="language-rs">fn str2u8(value: &str) -> u8 { if let Ok(v) = value.parse() { return v; } else { return 0; } } fn write(mut dh: DeviceHandle, cmd: &str) -> Result<String, String> { dh.set_active_configuration(1).unwrap(); if let Err(e) = dh.claim_interface(0) { println!("{}", e); return Err(e.to_string()); } else { // 字符串转 Vec<u8> let arr: Vec<&str> = cmd.split(",").collect(); let arr1: Vec<u8> = arr.iter().map(|value| str2u8(value)).collect(); if let Err(b) = dh.write_bulk(3, &arr1[..], Duration::new(60, 0)) { println!("{}", b); return Err(b.to_string()); } else { return Ok("执行成功".into()); } } }</code></pre> <h1>遇到的问题</h1> <ol> <li>一开始我是基于 <code>antd@5</code> 来做的,后来回家用 macOS 打包了,发现运行不起来,后来就把 <code>antd</code> 的版本降到 <code>4</code>。</li> <li>家里没有打印机,无法测试 macOS 版本的 app 是否正常。</li> <li>打包后,把安装包放到虚拟机里,发现缺少了 <code>libusb dll</code> 而导致程序无法正常运行,把 <code>libusb-1.0.dll</code> 拷贝到安装目录下就能正常运行。</li> <li>打包后,把安装包放到虚拟机(Windows 7)里运行,第一打开,由于系统没有安装 WebView2 运行时,安装程序会自动下载 WebView2 运行时。</li> <li>Windows <code>wix</code> 打包无法把 <code>libusb-1.0.dll</code> 打包进安装包,这个问题到现在也不知道怎样解决,如果你知道,请给我留言。</li> <li>Windows <code>nsis</code> 打包无法把 <code>Webviewloader.dll</code> 打包进安装包导致安装后无法运行。</li> </ol> <hr /> <p>2024-08-12 更新:</p> <p>对#3 #5 问题,改一下默认的编译工具链就行。</p> <p>1、列出本机编译工具链工具。<code>rustup toolchain list</code></p> <pre><code class="language-bash">$ rustup toolchain list stable-x86_64-pc-windows-gnu stable-x86_64-pc-windows-msvc (default)</code></pre> <p>如果默认的编译链工具不是 stable-x86_64-pc-windows-msvc,需要改为默认</p> <p>2、设置默认编译工具链</p> <pre><code class="language-bash">rustup default stable-msvc</code></pre>
详情
<p>如果有客人到访,问你要 wifi 密码,此时你有忘记了,你该怎么办?</p> <p>本文就给你支个招,教你如何快速查看已连接过的 wifi 密码,亲测可用。</p> <p>先说答案:通过“钥匙串访问” app 查看。</p> <h1>操作步骤</h1> <p>1、打开“钥匙串访问” app。"启动台" ->"其它"->“钥匙串访问”</p> <p>2、找到已经连接过的 wifi 的名称。比如我的是“ChinaNet-3CCW_2.4G_plus”</p> <p>3、右键->将密码拷贝到剪贴板。</p> <p>4、输入当前登录用户的密码。</p> <p>5、输入当前的登录用户名和密码。</p> <p>6、打开一个文本编辑器 Command + V 复制。即可看到该 wifi 对应的密码。</p> <p><img src="/uploads/wifi.png" alt="" /></p>
详情
<h1>起因</h1> <p>最近接到一个项目,需要基于 Fabric.js 实现一个表格,百度、Bing、谷歌搜索了一番,没有找到合适的,无法直接白嫖,那只能自己撸起袖子干。</p> <h1>原理分析</h1> <p>一个表格由一个个单元格组成,如下图。</p> <p><img src="/uploads/fabric-1.jpg" alt="image1" /></p> <p>从第二列开始,单元格的左边和前一列的单元右边重合; 从第二行开始,单元格的上边和前一行的单元格的底边重合; 那么一个障眼法的表格就出来了。</p> <h1>任务拆解</h1> <h2>实现单元格</h2> <p>在 Fabric.js 内,单元格可以用一个 <code>Group</code> 包住 <code>Rect</code> 和 <code>Textbox</code> 来实现。其中单元格的边框由<code>Rect</code> 的边框实现,单元格里面文本由 <code>Textbox</code> 实现。当双击单元格时,使 <code>Textbox</code> 进入编辑模式。</p> <pre><code class="language-js">// table.js import { fabric } from 'fabric' // 定义自定义的单元格类 fabric.TableCell = fabric.util.createClass(fabric.Group, { type: 'tableCell', text: null, rect: null, initialize: function (rectOptions, textOptions, text) { this.rect = new fabric.Rect(rectOptions) this.text = new fabric.Textbox(text, { ...textOptions, selectable: false, }) this.on('mousedblclick', () => { this.selectable = false this.text.selectable = true this.canvas.setActiveObject(this.text) this.text.enterEditing() }) this.on('mousedown', () => { // this.rect.set('fill', 'green') // this.canvas.requestRenderAll() }) this.text.on('editing:exited', () => { this.text.selectable = false this.selectable = true }) this.callSuper('initialize', [this.rect, this.text], { subTargetCheck: true, objectCaching: false }) }, toObject: function () { return fabric.util.object.extend(this.callSuper('toObject'), { text: this.text.get('text') }) } })</code></pre> <h2>实现表格</h2> <p>一个表格由一个 <code>Group</code> 包裹 <code>rowHeights.length</code> * <code>columnWidths.length</code> 个 <code>TableCell</code> 组成。 由于每一列的宽度会不一样,所以要算出下一列的 <code>left</code> 值;同理每一行的高度也会不一样,同样需要算出下一行所有单元格的 <code>top</code> 值</p> <p>由于会有单元格合并的情况存在,所以用 <code>ignore</code> 记住哪些单元格是不用渲染的。</p> <pre><code class="language-js">// table.js fabric.Table = fabric.util.createClass(fabric.Group, { type: 'table', borderWidth: 4, // 单元格边框宽度 rowHeights: [], // 行高,每一行的高度可以自由配置 columnWidths: [], // 列宽,每一列的宽度可以自由配置 cells: [], // 每一个单元格。比较重要的字段有 row, col 分别表示行、列的索引,从 0 开始,rowSpan 代表单元格横跨的行数,colSpan 代表单元格横跨的列数,content 代表单元格内的文字 initialize: function (tableOptions) { const { left, top, borderWidth, rowHeights, columnWidths, cells } = tableOptions const rectOptions = { rx: 0, stroke: '#000', fill: 'transparent', shadow: 0, strokeWidth: +borderWidth, strokeUniform: true } const textOptions = { lineHeight: 1, fontSize: 20, stroke: '#000', fill: '#000', selectable: false, textAlign: 'center', editingBorderColor: '#FF9002' } const ignore = [] const cellCmps = [] let totalH = 0 for (let i = 0; i < rowHeights.length; i++) { let totalW = 0 for (let j = 0; j < columnWidths.length; j++) { let { rowSpan = 0, colSpan = 0, content } = cells.find((cell) => cell.row == i && cell.col == j) || {} const height = rowHeights[i] const width = columnWidths[j] rowSpan = Math.min(rowHeights.length - 1, rowSpan) colSpan = Math.min(columnWidths.length - 1, colSpan) let w = width let h = height let rowSpanHeight = 0 let colSpanHeight = 0 //@TODO 以下 3 个 for 有很大的优化空间。 for (let rs = 1; rs <= rowSpan; rs++) { rowSpanHeight += rowHeights[i + rs] ignore.push(`${i + rs}:${j}`) } for (let rs = 1; rs <= colSpan; rs++) { colSpanHeight += columnWidths[j + rs] ignore.push(`${i}:${j + rs}`) } if (rowSpan > 0 && colSpan > 0) { for (let r = 1; r <= rowSpan; r++) { for (let c = 1; c <= colSpan; c++) { ignore.push(`${i + r}:${j + c}`) } } } h += rowSpanHeight w += colSpanHeight if (ignore.includes(`${i}:${j}`)) { totalW += width continue } const cell = new fabric.TableCell( { ...rectOptions, left: totalW + 1, top: totalH, width: w, height: h }, { ...textOptions, left: totalW + 1 + rectOptions.strokeWidth, top: totalH + rectOptions.strokeWidth, width: w, height: h }, content || `${i}-${j}` ) cellCmps.push(cell) totalW += width } totalH += rowHeights[i] } this.borderWidth = borderWidth this.rowHeights = rowHeights this.columnWidths = columnWidths this.cells = cells this.callSuper('initialize', cellCmps, { subTargetCheck: true, objectCaching: false, left, top }) }, toObject: function () { return fabric.util.object.extend(this.callSuper('toObject'), { borderWidth: this.get('borderWidth'), rowHeights: this.get('rowHeights'), columnWidths: this.get('columnWidths'), cells: this.get('cells') }) } }) fabric.Table.fromObject = function (o, callback) { const options = { left: o.left, top: o.top, rowHeights: o.rowHeights, columnWidths: o.columnWidths, borderWidth: o.borderWidth, cells: o.cells, } const newTable = new fabric.Table(options) callback(newTable) }</code></pre> <h1>Demo</h1> <pre><code class="language-demo.js">import { fabric } from 'fabric' class PcEditor { canvas; constructor(canvasId) { this.init(canvasId); } /** * 初始化画板 * @param {string} canvasId */ init(canvasId) { const canvas = new fabric.Canvas(canvasId, { stopContextMenu: true, controlsAboveOverlay: true, preserveObjectStacking: true, altSelectionKey: "altKey", }); this.canvas = canvas; this.addTable() } addTable() { const options = { left: 20, top: 20, borderWidth: 4, rowHeights: [80, 120], columnWidths: [160, 100, 100], cells: [ { row: 0, col: 0, rowSpan: 0, colSpan: 2, content: "遥遥领先" }, { row: 0, col: 1, rowSpan: 0, colSpan: 0, content: "" }, { row: 0, col: 2, rowSpan: 0, colSpan: 0, content: "" }, { row: 1, col: 0, rowSpan: 0, colSpan: 0, content: "" }, { row: 1, col: 1, rowSpan: 0, colSpan: 0, content: "" }, { row: 1, col: 2, rowSpan: 0, colSpan: 0, content: "" } ] }; const canvas = this.canvas; const center = canvas.getCenter(); const table = new fabric.Table(options); table.set("left", center.left - table.width / 2); table.set("top", center.top - table.height / 2); canvas.add(table); } } new PcEditor("canvasBox");</code></pre> <p><img src="/uploads/fabric-js-2.jpg" alt="image2" /></p> <h1>扩展阅读</h1> <ul> <li><a href="https://juejin.cn/post/7143794070513516581">Fabric.js 自定义子类,创建属于自己的图形~</a></li> <li><a href="https://stackoverflow.com/questions/74248303/editable-table-with-fabricjs">Editable table with fabricjs</a></li> </ul>
详情
<h1>解决方案</h1> <p>先说解决方案:</p> <pre><code>import { encodingIndexes } from "@zxing/text-encoding/es2015/encoding-indexes"; import { TextEncoder } from "@zxing/text-encoding"; // @ts-ignore window.TextEncodingIndexes = { encodingIndexes: encodingIndexes }; // 如果没有引入 encodingIndexes,机会报错:Indexes missing. Did you forget to include encoding-indexes.js first? // 当然,我这里是比较粗暴的做法,可能有更好的引入 encodingIndexes 的方法 const encoder = new TextEncoder("gb2312", { NONSTANDARD_allowLegacyEncoding: true, }); const encoded = encoder.encode("中文"); console.log(encoded); // Uint8Array(2) [214, 208, buffer: ArrayBuffer(2), byteLength: 2, byteOffset: 0, length: 2, Symbol(Symbol.toStringTag): 'Uint8Array']</code></pre> <p>转为 16 进制为 D6 D0,查看 GB2312 编码表为 D6D0 中</p> <h1>问题</h1> <p>打印英文不存乱码的问题。 打印机支持 gb2312 编码,从浏览发送数据到打印机,需要编码为 gb2312,如果发送到打印的字节数组是用 TextEncoder 的 encode("中") 编码出来,打印机打出的是 “涓 ”。</p> <pre><code class="language-js">const encoder = new TextEncoder(); const encoded = encoder.encode(“中”); console.log(encoded); // Uint8Array(3) [228, 184, 173, buffer: ArrayBuffer(3), byteLength: 3, byteOffset: 0, length: 3, Symbol(Symbol.toStringTag): 'Uint8Array']</code></pre> <p>[228, 184, 173] 转为 十六进制 E4 B8 AD,查 GB2312 编码表 E4 B8 -> 涓,AD 没有编码。</p> <p>乱码的原因是:打印机支持 gb2312,但是接收到的却是 utf-8 编码的文字,所以就乱码了。</p> <p>解决方法:统一两端的编码。</p> <h1>参考</h1> <p><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/TextEncoder">https://developer.mozilla.org/zh-CN/docs/Web/API/TextEncoder</a></p> <p><a href="https://juejin.cn/post/6844903534144552967">https://juejin.cn/post/6844903534144552967</a></p> <p><a href="https://github.com/zxing-js/text-encoding">https://github.com/zxing-js/text-encoding</a></p> <p><a href="http://tools.jb51.net/table/gb2312">GB2312 编码表</a></p>
详情
<p>为了面试,搜索了一些关于 fabric.js 的资料,花了一天的时间,做了个原型,之后利用业余时间断断续续地完善了一些功能。 目前已基本可用,就想和大家分享一下成果。</p> <h1>概述</h1> <p>首先我们来看看它的全貌 <img src="/uploads/label-editor.png" alt="标签编辑器" /></p> <p>如果想体验 Live demo 可以访问直接 <a href="https://www.pcdeng.com/tag-editor/">https://www.pcdeng.com/tag-editor/</a></p> <p>界面主要分四部分。</p> <ul> <li>顶栏,是 fabric.js 画布的对象(下文统称为组件)通用属性设置栏。</li> <li>左侧是组件列表,点击之后会把组件添加到画布中;图标组件由于有分组,需要选中某个分组下的图标再点击,才会添加到画布中。</li> <li>中间灰色背景部分是画布,白色区域是可见区域,就是说,只有白色画板内的组件才会可见,打印时也只会打印白色区域。</li> <li>右侧是组件详细属性设置栏。当没有选中组件时,可以设置白色画板的大小。当有组件被选中时,展示的是选中组件特有的属性。</li> </ul> <h1>功能清单</h1> <ul> <li>支持在线字体</li> <li>离线可用(PWA)</li> <li>支持文字</li> <li>支持矩形</li> <li>支持线条</li> <li>支持条码</li> <li>支持二维码</li> <li>支持图标</li> <li>支持图片</li> <li>支持表格</li> <li>支持组合</li> <li>支持拆分组合</li> <li>支持图层</li> <li>支持单位转换(工具)</li> </ul> <h1>技术栈</h1> <p>前端技术栈:Vue3、Naive UI、Vite、fabric.js</p> <h1>参考</h1> <ul> <li><a href="https://github.com/nihaojob/vue-fabric-editor" target="_blank">vue-fabric-editor</a></li> <li><a href="https://juejin.cn/column/7050370347324932132" target="_blank">零基础入门Fabric.js,提高Canvas开发效率</a></li> </ul>
详情
<h1>概述</h1> <p>Chrome extension 又称 Chrome 插件,Chrome 扩展程序,为方便叙述,下文用“ Chrome 扩展”指代。你可以从<a href="https://chrome.google.com/webstore"> Chrome 应用商店</a>获取 <img src="/uploads/chrome-web-store.png" alt="" />,也可以离线包的方式安装,比如<a href="https://juejin.cn/extension?utm_source=jj_nav">掘金浏览器插件</a>,<a href="https://note.youdao.com/note-download">有道云网页剪报插件</a>,<a href="https://www.wetab.link/">Wetab</a></p> <h2>框图</h2> <p><img src="/uploads/chrome-extension.png" alt="框图" /></p> <p>我做的一个 <a href="https://chrome.google.com/webstore/detail/pos-printer-manager/ngmlmeaknfjiomiklcdjmoajhdompmii">插件</a> 已经上架到 Chrome 应用商店,它是一个打印机驱动程序。</p> <h2>Chrome 扩展能做什么</h2> <ul> <li>拦截网络请求,比如比较出名的 ad blocker</li> <li>调试工具,如 Vue Devtools,React Developer Tools,Angular Prober</li> <li>标签管理,如 WeTab,Bookmarks clean up</li> <li>打印机驱动,如 POS Printer manager,Zebra Printing</li> <li>截图,如 滚动屏幕截图工具和屏幕捕获</li> </ul> <h1>基本概念</h1> <p>Chrome 扩展目前最新的版本是 v3,网络上有些资料是 v2 的,阅读的时候要注意区分一下。</p> <ul> <li>manifest.json Chrome 扩展清单文件,关于 Chrome 扩展的有关配置都在这个文件内,比如名字,描述,版本,图标。其中 manifest_version 字段值为 3,如果是 2,是无法发布到 Chrome 应用商店的。</li> <li>Service workers Manifest V3 将 background pages 替换为 service worker。监听大部分浏览器事件然后做出处理,这些事件如 新打开标签页、关闭标签页、网络请求完成、删除书签、新增书签等等。</li> <li>popup html <img src="/uploads/webtab-popup-html.png" alt="" /></li> <li>content script 可以嵌入到每一个页面的脚本</li> </ul> <h1>开发一个简单扩展</h1> <p>前端开发中,和后端对接经常会遇到跨域的问题。解决方法要么在前端的 devServer 配置转发,要么要求后端配置允许跨域。这个插件实现的是修改响应头,模拟后端允许跨域。</p> <h2>创建一个 manifest.json</h2> <pre><code class="language-json">// manifest.json {  "manifest_version": 3,  "name": "Proxy",  "description": "Enable CORS for each api reqeust",  "version": "1.0" }</code></pre> <h2>配置规则</h2> <pre><code class="language-json">// manifest.json "host_permissions": ["http://localhost:9527/*"], "declarative_net_request": { "rule_resources": [ { "id": "rules", "enabled": true, "path": "rules.json" } ] }, "permissions": ["declarativeNetRequest", "declarativeNetRequestFeedback"]</code></pre> <pre><code class="language-json">// rules.json [ { "id": 1, "priority": 1, "condition": { "urlFilter": "*", "resourceTypes": ["xmlhttprequest", "script", "main_frame", "image"] }, "action": { "type": "modifyHeaders", "responseHeaders": [ { "header": "Access-Control-Allow-Origin", "operation": "set", "value": "*" } ] } } ]</code></pre> <h1>参考</h1> <p><a href="https://juejin.cn/post/7223315092858224695">Chrome扩展开发指南</a></p>
详情
<p>最近在做集成腾讯广告的项目,碰到一个交互挺复杂的功能。用了一个上午的时间做了一个原型。</p> <p>这种数据结构应该怎样实现呢?</p> <p>我实现的思路是,一个表单就是数组里的一项,每个表单根据 templateId 来分组。</p> <p>allTabs -> tabs -> currentTab(formData)</p> <p><code>allTabs</code> 是一个数组,里面存的是一个对象,<code>templateId</code> 是对应上面的分组的唯一 <code>id</code> (如“常规图片1图”的 <code>templateId</code> 是 <code>1</code>)。</p> <p><code>formData</code> 就是当前选中的 <code>tab</code> 的表单数据对象。</p> <pre><code>{ "templateId": 2, "formData": { "name": "", "region": "", "date1": "", "date2": "", "delivery": false, "type": [], "resource": "", "desc": "" } }</code></pre> <p><img src="//www.pcdeng.com/uploads/vue-ad.png" alt="" /></p> <p><a href="//www.pcdeng.com/vue-ad/">演示地址</a></p>
详情
<h1>stm32-demo</h1> <h2>前置准备</h2> <table> <thead> <tr> <th>项</th> <th>备注</th> </tr> </thead> <tbody> <tr> <td>操作系统</td> <td>macOS 10.14.6</td> </tr> <tr> <td>编辑器</td> <td>Visual Studio Code</td> </tr> <tr> <td>目标芯片</td> <td>STM32F103C8T6</td> </tr> <tr> <td>仿真器</td> <td>CMSIS-DAP</td> </tr> </tbody> </table> <p>开始之前请确保你的电脑已经安装了:Rust 和 openocd</p> <h2>拉取代码</h2> <p><code>git clone git@github.com:pcdeng/stm32-demo.git</code></p> <h2>Visual Studio Code 安装 rust-analyzer</h2> <p><a href="https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer">rust-analyzer Visual Studio Code 插件</a></p> <h2>编译</h2> <p><code>cargo build</code> 编译出的是 <code>debug</code> 模式的,<code>target/thumbv7m-none-eabi/debug/stm32demo</code> 就是可以下载到开发板的文件,文件大小比较大。</p> <p><code>cargo build --release</code> 编译出的是 <code>release</code> 模式的。<code>target/thumbv7m-none-eabi/release/stm32demo</code> 是可以下载到开发板的文件。文件大小比 <code>debug</code> 模式下的小很多。</p> <h2>接上调试器</h2> <p><img src="/uploads/link.jpeg" alt="" /></p> <h2>开启 openocd 服务</h2> <p><code>openocd -f ./openocd.cfg</code> 成功启动 openocd 服务的话,会出现类似如下信息</p> <pre><code>Open On-Chip Debugger 0.11.0 Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Info : Listening on port 6666 for tcl connections Info : Listening on port 4444 for telnet connections Info : CMSIS-DAP: SWD Supported Info : CMSIS-DAP: JTAG Supported Info : CMSIS-DAP: FW Version = 2.0.0 Info : CMSIS-DAP: Interface Initialised (SWD) Info : SWCLK/TCK = 1 SWDIO/TMS = 1 TDI = 0 TDO = 1 nTRST = 0 nRESET = 1 Info : CMSIS-DAP: Interface ready Info : clock speed 1000 kHz Info : SWD DPIDR 0x1ba01477 Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints Info : starting gdb server for stm32f1x.cpu on 3333 Info : Listening on port 3333 for gdb connections</code></pre> <h2>登录 telnet 服务</h2> <p>在另一个终端窗口执行 <code>telnet 127.0.0.1 4444</code> 端口 <code>4444</code> 是从上一步的日志信息获取的。<code>Info : Listening on port 4444 for telnet connections</code></p> <h3>停机</h3> <p>在 telnet 那个终端输入 <code>halt</code></p> <h3>写入</h3> <p><code>flash write_image erase ./target/thumbv7m-none-eabi/debug/stm32demo</code></p> <h3>重启</h3> <p><code>reset</code></p> <h1>参考资料</h1> <p><a href="https://www.rust-lang.org/zh-CN/tools/install">安装 Rust</a> <a href="https://doc.rust-lang.org/stable/embedded-book/intro/install/macos.html">macOS Rust 嵌入式需要安装的软件</a> 我没有安 QEMU 因为我有开发板</p> <p><a href="https://detail.tmall.com/item.htm?_u=3ko4pss5f42&id=631928588828&spm=a1z09.2.0.0.d5592e8dy0qIbk">我用的开发板</a></p> <p><a href="https://jzow.github.io/discovery/microbit/index.html">Discovery</a></p> <p><a href="https://github.com/stm32-rs/stm32f1xx-hal">stm32f1xx-hal</a></p> <p><a href="https://space.bilibili.com/500416539/channel/collectiondetail?sid=177577&ctype=0">Rust嵌入式开发入门</a> 强烈推荐,作者解说得非常详细。</p>
详情
<pre><code class="language-vue"><template> <div class="chart-wrapper"> <div ref="chartRef" class="chart"></div> </div> </template> <script lang="ts" setup> import * as echarts from 'echarts'; import { onMounted, ref } from 'vue'; import { EChartsOption } from 'echarts'; import GD_GEO_JSON from './440000_full.json'; const PROVINS = [ { name: '北京市', value: 20057.34 }, { name: '天津市', value: 15477.48 }, { name: '河北省', value: 31686.1 }, { name: '山西省', value: 6992.6 }, { name: '内蒙古自治区', value: 44045.49 }, { name: '辽宁省', value: 40689.64 }, { name: '吉林省', value: 37659.78 }, { name: '黑龙江省', value: 45180.97 }, { name: '上海市', value: 55204.26 }, { name: '江苏省', value: 21900.9 }, { name: '浙江省', value: 4918.26 }, { name: '安徽省', value: 5881.84 }, { name: '福建省', value: 4178.01 }, { name: '江西省', value: 2227.92 }, { name: '山东省', value: 2180.98 }, { name: '河南省', value: 9172.94 }, { name: '湖北省', value: 3368 }, { name: '湖南省', value: 806.98 }, { name: '广东省', value: 40689.44 }, ]; const CITIES = [ { name: '广州市', value: 1 }, { name: '韶关市', value: 20 }, { name: '深圳市', value: 30 }, { name: '珠海市', value: 30 }, { name: '汕头市', value: 40 }, { name: '佛山市', value: 44 }, { name: '江门市', value: 55 }, { name: '湛江市', value: 66 }, { name: '茂名市', value: 77 }, { name: '肇庆市', value: 88 }, { name: '惠州市', value: 99 }, { name: '梅州市', value: 11 }, { name: '汕尾市', value: 22 }, { name: '河源市', value: 33 }, { name: '阳江市', value: 44 }, { name: '清远市', value: 55 }, { name: '东莞市', value: 66 }, { name: '中山市', value: 77 }, { name: '潮州市', value: 88 }, { name: '揭阳市', value: 99 }, { name: '云浮市', value: 33 }, ]; const convertData = () => { const cities = GD_GEO_JSON.features.map(item => item.properties); return CITIES.map(city => { const target = cities.find(c => c.name === city.name); return { ...city, lng: target?.centroid[0], lat: target?.centroid[1], }; }); }; const finalData = convertData(); const option: EChartsOption = { title: { text: '中国', padding: 10, }, tooltip: { trigger: 'item', }, geo: { tooltip: { show: true, }, map: 'CN', roam: false, // 是否支持缩放 label: { show: false, }, emphasis: { label: { show: false, }, }, }, dataset: { dimensions: ['name', 'value', 'lat', 'lng'], source: finalData, }, series: [ { name: '广场', type: 'map', map: 'CN', label: { show: false, }, emphasis: { label: { show: false, }, }, tooltip: { show: false, }, data: CITIES, }, { type: 'effectScatter', coordinateSystem: 'geo', symbolSize: 10, itemStyle: { color: '#2875FB', }, encode: { name: 'name', lng: 'lng', lat: 'lat', value: 'value', }, label: { formatter: '{@value}个广场', position: 'top', show: true, color: '#fff', padding: [0, 8], height: 30, verticalAlign: 'middle', borderRadius: 4, backgroundColor: 'rgba(0,0,0,0.65)', }, tooltip: { show: false, }, }, ], visualMap: { min: 1, max: 100, text: ['High', 'Low'], realtime: false, calculable: true, inRange: { color: ['lightskyblue', 'yellow', 'orangered'], }, show: false, seriesIndex: 0, }, }; const chartRef = ref(); echarts.registerMap('CN', GD_GEO_JSON as any); const pos = ref({ x: 0, y: 0 }); onMounted(() => { const myChart = echarts.init(chartRef.value); myChart.setOption(option); }); </script> <style lang="scss" scoped> .chart-wrapper { position: relative; height: 100%; width: 100%; box-sizing: border-box; } .chart { width: 100%; height: 100%; margin: 0 auto; box-sizing: border-box; } </style></code></pre> <h1>参考</h1> <p><a href="https://echarts.apache.org/examples/zh/editor.html?c=map-HK">https://echarts.apache.org/examples/zh/editor.html?c=map-HK</a></p> <p>获取广东省的 GEO data</p> <p><a href="http://datav.aliyun.com/portal/school/atlas/area_selector">http://datav.aliyun.com/portal/school/atlas/area_selector</a></p>
详情
<h1>app 的名字</h1> <p>Lite Hotel,虽叫 lite,但功能还是挺全的。</p> <h1>功能</h1> <h2>支持更新任务状态</h2> <h2>支持更换任务经办人</h2> <h2>工单管理</h2> <h2>客房管理</h2> <h2>消息管理</h2> <h3>支持消息推送</h3> <h3>支持消息一键标为已读</h3> <h3>支持批量删除消息</h3> <h2>其它</h2> <h3>支持多语言</h3> <h3>支持暗黑模式</h3> <h3>支持在线更新</h3> <h3>支持消息推送</h3> <h1>开发进度表</h1> <p><img src="https://www.pcdeng.com/uploads/mini_hotel_progress.png" alt="" /></p> <h2>实际效果图</h2> <p><img src="https://www.pcdeng.com/uploads/mini_hotel_home.png" alt="" /> <img src="https://www.pcdeng.com/uploads/rooms.png" alt="" /> <img src="https://www.pcdeng.com/uploads/notifications.png" alt="" /> <img src="https://www.pcdeng.com/uploads/my.png" alt="" /> <img src="https://www.pcdeng.com/uploads/settings.png" alt="" /> <img src="https://www.pcdeng.com/uploads/select_locale.png" alt="" /></p> <h1>app 下载</h1> <p><a href="//www.pcdeng.com/uploads/app.apk">apk</a></p> <h1>web 管理端</h1> <p><img src="//www.pcdeng.com/uploads/web.png" alt="" /></p> <p>今天百度了一下才知道,Mini Hotel 已经有人做了。也有一个网站,而且做的功能更多,它的地址是:<a href="https://minihotel.io">https://minihotel.io</a> 这个名字不能用了,迟点要统一改掉。</p> <p>如果想体验一下,可以联系我要演示账号。 <img src="https://www.pcdeng.com/uploads/wechat.png" alt="qrcode" /></p>
详情
<p>最近工作没有那么忙,就抽时间整理了之前写过的一些例子,整理如下: <a href="https://pcdeng.github.io/">https://pcdeng.github.io/</a></p>
详情
<h1>需求</h1> <ul> <li>A 邀请 B, B 邀请 C,统计 A 的会员数。</li> <li>B 下单的总数</li> <li>默认排序按用户注册时间,支持按会员数降序、会员下单数降序排序。</li> </ul> <h1>表结构</h1> <pre><code># user 表 +-------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+--------------+------+-----+---------+----------------+ | id | int unsigned | NO | PRI | NULL | auto_increment | | name | varchar(255) | YES | | NULL | | | dateline | int | YES | | NULL | | | inviter_uid | int | YES | | NULL | | +-------------+--------------+------+-----+---------+----------------+ # user_order 表 +----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+----------------+ | id | int unsigned | NO | PRI | NULL | auto_increment | | user_id | int unsigned | YES | | NULL | | | name | varchar(255) | YES | | NULL | | | dateline | int | YES | | NULL | | +----------+--------------+------+-----+---------+----------------+</code></pre> <h1>实现</h1> <pre><code class="language-php"><?php namespace app\index\controller; use think\Controller; use Db; class Index { protected static $mapping = ['time' => 'dateline', 'member' => 'memberCount', 'order' => 'orderCount']; public function hello() { // 处理 sort 排序参数 $sort = request()->param('sort', 'time'); $sorts = array_keys(self::$mapping); if (!in_array($sort, $sorts)) { die('参数不合法'); } $sort = self::$mapping[$sort]; $model = Db(); $users = $model->query("SELECT u.id, u.dateline, DATE_FORMAT(FROM_UNIXTIME(u.dateline), '%Y-%m-%d %H:%i:%s') time, u.NAME, ( SELECT count( o.id ) FROM user_order o WHERE o.user_id = u.id ) 'orderCount', ( SELECT count( uu.id ) FROM USER uu WHERE ( uu.inviter_uid IN ( SELECT uuu.id FROM USER uuu WHERE uuu.inviter_uid = u.id )) OR uu.inviter_uid = u.id ) 'memberCount' FROM USER u order by $sort DESC"); dump($users); } }</code></pre> <h1>测试</h1> <p>浏览器访问</p> <p><a href="http://127.0.0.1:8083/public/index.php?s=index/index/hello">http://127.0.0.1:8083/public/index.php?s=index/index/hello</a></p> <p><a href="http://127.0.0.1:8083/public/index.php?s=index/index/hello&sort=time">http://127.0.0.1:8083/public/index.php?s=index/index/hello&sort=time</a></p> <p><a href="http://127.0.0.1:8083/public/index.php?s=index/index/hello&sort=order">http://127.0.0.1:8083/public/index.php?s=index/index/hello&sort=order</a></p> <p><a href="http://127.0.0.1:8083/public/index.php?s=index/index/hello&sort=member">http://127.0.0.1:8083/public/index.php?s=index/index/hello&sort=member</a></p>
详情