【油猴脚本教程】P2 初识油猴脚本 & 油猴脚本的基本构成
P2 初识油猴脚本 教程索引 视频链接 光讲JS感觉不是特别直观，用两个不涉及网络请求的脚本进行讲解 第一个脚本 - Remove Live.js 脚本链接： https://github.com/chr233/GM_Scripts/blob/master/Bilibili/Remove_Live.js 脚本功能： 提供一个开关，用于移除直播播放器，并且带有记忆功能 源码： // ==UserScript== // @name Remove_Live // @name:zh-CN 去除B站直播间播放器 // @namespace https://blog.chrxw.com // @version 1.1 // @description 去除B站多余的直播播放器（滑稽） // @author Chr_ // @include https://live.bilibili.com/* // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // ==/UserScript== (() => { "use strict"; //去除播放器的开关 let VEnable = localStorage.getItem("VEnable") === "true"; if (VEnable) { setTimeout(() => { document.getElementById("live-player").remove(); }, 3000); } let btnArea = document.querySelector(".right-ctnr"); let btn = document.createElement("button"); btn.textContent = VEnable ? "恢复播放器" : "移除播放器"; btn.addEventListener("click", () => { VEnable = !VEnable; localStorage.setItem("VEnable", VEnable); btn.textContent = VEnable ? "恢复播放器" : "移除播放器"; if (VEnable) { document.getElementById("live-player").remove(); } else { location.reload(); } }); btnArea.insertBefore(btn, btnArea.children[0]); })(); 第二个脚本 - SteamDB CN.js 脚本链接： https://github.com/chr233/GM_Scripts/blob/master/SteamDB/SteamDB_CN.js 脚本功能： 汉化SteamDB 源码： // ==UserScript== // @name:zh-CN SteamDB汉化 // @name SteamDB_CN // @namespace https://blog.chrxw.com // @supportURL https://blog.chrxw.com/scripts.html // @contributionURL https://afdian.net/@chr233 // @version 1.32 // @description SteamDB汉化插件 // @description:zh-cn SteamDB汉化插件 // @author Chr_ // @match https://steamdb.info/* // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // @resource data https://cf.chrxw.com/SteamDB/SteamDB_CN.json // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_registerMenuCommand // ==/UserScript== (() => { "use strict"; //是否打开调试模式（使用自定义汉化规则而不是在线规则） const DEBUG = localStorage["dbg_mode"] == "开"; //是否在控制台输出未汉化文本 const OUTPUT = localStorage["out_word"] == "开"; GM_registerMenuCommand(`调试汉化文本: 【${DEBUG ? "开" : "关"}】`, () => { window.localStorage["dbg_mode"] = DEBUG ? "关" : "开"; window.location.reload(); }); GM_registerMenuCommand(`在控制台输出未匹配文本: 【${OUTPUT ? "开" : "关"}】`, () => { window.localStorage["out_word"] = OUTPUT ? "关" : "开"; window.location.reload(); }); document.querySelector("html").setAttribute("lang", "zh-CN"); let Locales; if (DEBUG) { const template = `{"DOC":{"更新时间":"调试模式","贡献名单":["调试模式"]},\n"STATIC":\n{\n\n},\n"INPUT":\n{\n\n},\n"LABEL":\n{\n\n},\n"DYNAMIC":\n{\n\n}\n}`; const box = document.createElement("div"); box.className = "sdc"; const text = document.createElement("textarea"); box.appendChild(text); const action = document.createElement("div"); action.className = "sdc-links"; box.appendChild(action); const btnSave = document.createElement("a"); btnSave.innerText = "💾 保存并应用"; btnSave.addEventListener("click", () => { const raw = text.value.trim(); if (!raw) { alert("翻译文本不能为空!!!"); } else { try { JSON.parse(raw); window.localStorage["sdb_lang"] = raw; window.location.reload(); } catch (e) { alert("翻译文本不是有效的JSON格式!!!"); } } }); action.appendChild(btnSave); const btnReset = document.createElement("a"); btnReset.textContent = "🗑️ 清空文本"; btnReset.addEventListener("click", () => { window.localStorage["sdb_lang"] = template; window.location.reload(); }); action.appendChild(btnReset); const btnOnline = document.createElement("a"); btnOnline.textContent = "📄 当前在线文本"; btnOnline.addEventListener("click", () => { if (confirm("替换为在线版本后当前所做修改将会丢失, 确定要继续吗?")) { text.value = GM_getResourceText("data"); } }); action.appendChild(btnOnline); const about = document.createElement('a'); about.href = "https://blog.chrxw.com" about.innerText = "🔗 By Chr_ © 2022"; action.appendChild(about); const father = document.getElementById("main"); father.insertBefore(box, father.firstChild); const customLang = window.localStorage["sdb_lang"] ?? template; text.value = customLang; Locales = JSON.parse(customLang); } else { Locales = JSON.parse(GM_getResourceText("data")); } //计时 const Start = new Date().getTime(); {//静态元素 for (const [css, dic] of Object.entries(Locales.STATIC)) { if (OUTPUT) { console.log(`〖${css}〗`); } const elements = document.querySelectorAll(css); if (elements.length > 0) { for (let i = 0; i < elements.length; i++) { const element = elements[i]; if (element.childElementCount === 0) {//节点内部无其他元素 const raw = element.innerText?.trim(); if (!raw || raw.length <= 2) { continue; } const txt = dic[raw]; if (txt) { element.innerText = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } else {//节点内部有其他元素 const nodes = element.childNodes; for (let j = 0; j < nodes.length; j++) { const node = nodes[j]; if (node.nodeType === Node.TEXT_NODE) { const raw = node.textContent?.trim(); if (!raw || raw.length <= 2) { continue; } const txt = dic[raw]; if (txt) { node.textContent = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } } } } else { if (OUTPUT) { console.warn(`CSS选择器未匹配到任何元素: ${css}`); } } } } {//输入框 const inputs = Locales.INPUT; if (OUTPUT) { console.log("〖输入框〗"); } const elements = document.querySelectorAll("input"); for (let i = 0; i < elements.length; i++) { const element = elements[i]; const raw = element.placeholder; if (!raw) { continue; } const txt = inputs[raw]; if (txt) { element.placeholder = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } {//悬浮提示 const labels = Locales.LABEL; if (OUTPUT) { console.log("〖提示文本〗"); } const elements = document.querySelectorAll("*[aria-label]"); for (let i = 0; i < elements.length; i++) { const element = elements[i]; const raw = element.getAttribute("aria-label"); if (!raw) { continue; } const txt = labels[raw]; if (txt) { element.setAttribute("aria-label", txt); } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } const { script: { version } } = GM_info; //const version = GM_info.script.version; const { DOC: { "更新时间": update, "贡献名单": contribution } } = Locales; const End = new Date().getTime(); // 统计耗时 console.log("执行耗时", `${End - Start} ms`); console.log("================================="); console.log(`插件版本: ${version}`); console.log(`更新时间: ${update}`); console.log(`贡献名单: ${contribution.join(", ")}`); console.log("================================="); console.log("迷茫同学:\n『没有恶意 请问直接用谷歌翻译整个网页不香吗』") // 添加按钮 const headerUl=document.querySelector(".header-menu-container>div>ul:nth-child(1)"); const footerUl=document.querySelector(".footer-container>div>ul:nth-child(1)"); const scriptLink = document.createElement("li"); scriptLink.innerHTML = `SteamDB 汉化 V${version}`; headerUl.appendChild(scriptLink); footerUl.appendChild(scriptLink.cloneNode(true)); // 添加样式 GM_addStyle(` .tabnav-tabs > a { min-width: 80px; } .sdc { display: flex; } .sdc > textarea { width: 100%; height: 200px; min-height: 200px; resize: vertical; } .sdc > div.sdc-links { width: 150px; margin: 5px; } .sdc > div.sdc-links > a { width: 100%; margin-bottom: 10px; background-color: #213145; color: white; font-size: 12px; border-radius: 0px; display: inline-block; height: 30px; } .sdc > div.sdc-links > a:last-child { width: 100%; margin-top: 30px; margin-bottom: 0px; color: #67c1f5; background-color: #273b4b; } `); })(); 油猴脚本基本构成 元信息 记录脚本的额外信息，供油猴插件以及 GreasyFork 使用 // ==UserScript== // @name:zh-CN 脚本名称(可选，可以使用语言代码来支持多语言) // @name 脚本名称 // @namespace 命名空间 // @version 脚本版本 // @description 脚本描述 // @description:zh-cn 脚本描述(可选，可以使用语言代码来支持多语言) // @author 作者 // @include 脚本生效域名 // @match 脚本生效域名 // @license 共享协议 // @icon 脚本图标(可选) // @resource 外部资源链接，以及访问名称(可选) // @grant 申请额外权限(可选) // ==/UserScript== 脚本正文 略
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 前几天研究某APP未果，中间稍微研究了一些Frida的RPC机制，做一下笔记。 Frida RPC简介 Frida RPC可以将APP中的函数暴露给加载器，方便在外部调用，相比于recv和send，使用起来更加方便。 示例hook脚本 rpc.exports = { add(a, b) { //同步函数 return a + b; }, sub(a, b) { //异步函数 return new Promise(resolve => { setTimeout(() => { resolve(a - b); }, 100); }); } }; 示例Python加载器 import codecs import frida def on_message(message, data): if message['type'] == 'send': print(message['payload']) elif message['type'] == 'error': print(message['stack']) session = frida.attach('iTunes') with codecs.open('./agent.js', 'r', 'utf-8') as f: source = f.read() script = session.create_script(source) script.on('message', on_message) script.load() #调用rpc暴露出来的add和sub方法 print(script.exports.add(2, 3)) print(script.exports.sub(5, 3)) session.detach() Frida RPC使用演示 Java层 Java函数的Hook又分为静态方法和实例方法，静态类直接用Java.use('类名')获取以后直接就可以调用，而实例方法需要先用Java.choose('类名')，在堆中查找到类的实例以后获取该实例，才能调用。 静态方法 实例方法 So层 SO层如果是静态方法也跟Java层的类似，只不过需要获取上下文对象，并作为第一个参数传入。 JNI方法 演示代码 Hook代码 console.log("脚本载入成功"); Java.perform(function () { rpc.exports = { staticmethod() { var n1 = Java.use('com.max.xiaoheihe.utils.n1'); return n1.E(); }, classmethod() { var result = undefined; Java.choose('com.max.xiaoheihe.bean.account.AccountDetailObj', { onMatch: function (instance) { result = instance.getUserid(); }, onComplete: function () { if (result === undefined) { return 'not found'; } } }); return result; }, jnimethod(urlpath, timestamp, nonce) { var currentApp = Java.use('android.app.ActivityThread').currentApplication(); var context = currentApp.getApplicationContext(); var NDKTools = Java.use('com.max.xiaoheihe.utils.NDKTools'); return NDKTools.encode(context, urlpath, timestamp, nonce); }, }; }); 加载器 import frida device = frida.get_usb_device() process = device.attach('小黑盒') with open('demo.js', 'r', encoding='utf-8') as f: script = process.create_script(js.read()) script.load() print('static method: ', script.exports.staticmethod()) print('class method: ', script.exports.classmethod()) print('jni method: ', script.exports.jnimethod( '/app/link/tree/', '1637135684', '00000000000000000000000000000000')) 运行结果如下：
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] [post cid="1365"/] [post cid="1383"/] 前情提要 距离上一篇文章已经过去一年多了，前两天心血来潮找了最新版本研究了一下，熬了两天夜也没整明白，不打算继续研究下去了，做个笔记记录下分析过程。 @> 本文适用于小黑盒 1.3.191，小黑盒历史版本参考我的仓库 @> 全文评论后可见
前言 总共花了4天休息时间做的小玩意，支持显示通用TOTP令牌(6位数字)和STEAM令牌(5位)，同时也是个桌面时钟。 源码获取：GitHub 令牌原理 i> 简单的介绍一下两个令牌的原理，不感兴趣的可以参考下一节 原理 令牌类似于短信验证码，是一种一次性密码OTP，核心原理很简单，服务器和用户事先约定好一个Secret，然后用户通过特殊的算法（一般用Secret + 时间戳或者计数器作为参数），得到一串字符，服务器也通过同样的算法得到一串字符，如果两者相同，那么就证明了我是我。 常见的令牌大多是基于时间的令牌TOTP，也就是用当前的时间戳作为输入，只要保证和服务端时间差不超过令牌有效期（常见的实现是30秒）就能确保通过验证。优点是令牌客户端可以不需要联网，缺点是可能因为时间差导致验证失败。 另一种实现是基于HMAC的令牌HOTP，这种令牌需要和服务器约定一个计数器，客户端刷新一次令牌，就告诉服务器，让两边的计数器同步，这样用户和服务器得到的结果才能一样，才能通过验证。缺点显而易见，就是令牌客户端需要联网。 STEAM的令牌系统类似于TOTP，也是基于时间戳的，只是算法略有区别，最明显的区别是普通TOTP计算得到的是6位数字，STEAM令牌是5位数字或字母。 x> Secret一定要保管好，千万不要向他人透露。 i> 完整代码位于utils/twofa.py TOTP详解 伪代码 #. 演示用的中间结果用【】 表示 0. 服务器和用户事先约定了一个密钥Secret，记为K【MNUHE6DXGEZDGNBVGY======】(Base32编码，也有的实现采用Base64编码） 1. 取当前时间戳/30，取整后记为C【53914423】 2. 使用C作为消息，K作为密钥，哈希方式一般为Sha-1，计算HMAC哈希值H【deb27e0ae7e70fc2d2527e6ed231967e26503f72】(16进制) 3. 取H最后一个字节的低4位，用作偏移，记为O【2】(即16进制C的最后一位) 4. 取H的第O到O+4字节【7e0ae7e7】，丢弃最高位，变为32位无符号整数，记为I【7e0ae7e7】(最高位本来就是0，所以不变) 5. 把I转成10进制数字【17602563747】，最低6位就是结果【643943】 Python实现 import hmac import hashlib from base64 import b32decode from time import time import struct def get_totp_auth_code(secret: str, t: int = None): if not t: t = int(time() / 30) key = b32decode(secret) msg = struct.pack(">Q", t) mac = hmac.new(key, msg, hashlib.sha1).digest() offset = mac[-1] & 0x0f binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff code = str(binary)[-6:].zfill(6) return code # 演示DEMO print(get_totp_auth_code('MNUHE6DXGEZDGNBVGY======',53914423)) # 输出结果为 643943 Steam令牌详解 伪代码 #. 演示用的中间结果用【】 表示 0. 服务器和用户事先约定了一个密钥Secret，记为K【Y2hyeHcxMjM0NTY=】(Base64编码） 1. 取当前时间戳/30，先取整【53914423】，然后转换成8字节，前方补0，记为C【000000000336ab37】(16进制) 2. 使用C作为消息，K作为密钥，哈希方式一般为Sha-1，计算HMAC哈希值H【deb27e0ae7e70fc2d2527e6ed231967e26503f72】(16进制) 3. 取H最后一个字节的低4位，用作偏移，记为O【2】(即16进制C的最后一位) 4. 取H的第O到O+4字节【7e0ae7e7】，丢弃最高位，变为32位无符号整数，记为I【7e0ae7e7】(最高位本来就是0，所以不变)，为了方便计算转成10进制为【2114643943】 5. 创建一个字母表G【23456789BCDFGHJKMNPQRTVWXY】(去掉了一些容易混淆的字符) 6. 用I【2114643943】对26取余，记为M【9】，然后将I整除26，重复5次，得到5个M【9,13,7,12,25】(10进制) 7. 把5个M作为下标，从G中取出对应位置的字符，最后拼接起来就是结果【CH9GY】 i> 两个算法0~4步都是一模一样的，只是steam的算法多了一步处理过程 Python实现 import hmac import hashlib from base64 import b64decode from time import time import struct def get_steam_auth_code(secret: str, t: int = None) -> str: if not t: t = int(time()/30) msg = struct.pack(">Q", t) key = b64decode(secret) mac = hmac.new(key, msg, hashlib.sha1).digest() offset = mac[-1] & 0x0f binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff codestr = list('23456789BCDFGHJKMNPQRTVWXY') chars = [] for _ in range(5): chars.append(codestr[binary % 26]) binary //= 26 code = ''.join(chars) return code # 演示DEMO print(get_steam_auth_code('Y2hyeHcxMjM0NTY=',53914423)) # 输出结果为 CH9GY 屏幕显示 i> 我使用的是微雪的2.7寸墨水屏，也可以使用别的尺寸的屏幕，自行修改图片生成函数进行适配即可。 买屏幕的时候直接买适配树莓派的就行了，基本上都提供Python的驱动，直接用就行，非常方便。 !> 常见的屏幕一般是SPI接口，树莓派默认是不开启SPI接口的，可以使用官方的工具rasbpi-config开启，或者手动编辑/boot/config.txt，在最后一行加一个dtparam=spi=on，重启后生效。 img.py i> 如果对PIL绘图不是很熟悉可以看下一节。 ''' # @Author : Chr_ # @Date : 2021-03-30 17:04:24 # @LastEditors : Chr_ # @LastEditTime : 2021-04-03 00:26:15 # @Description : 图片生成器 ''' import time from os import path from typing import List, Tuple from PIL import Image, ImageDraw, ImageFont SCREEN_W = 264 # 屏幕显示宽度 SCREEN_H = 176 # 屏幕显示高度 CODE_W = 115 # 中间区域宽度 CODE_H = 27 # 中间区域一条令牌的高度 ICON_W = 20 # 左边图标区域宽度 ICON_H = 44 # 左边图标区域一个图标的高度(包含空白) CLOCK_W = SCREEN_W-ICON_W - CODE_W CLOCK_H = SCREEN_H FONT_CODE = ImageFont.truetype(path.join('font', 'Courier Prime Code.ttf'), 25) # 令牌字体 FONT_TAG = ImageFont.truetype(path.join('font', 'CascadiaMono.ttf'), 12) # 令牌名称的字体 FONT_PAGE = ImageFont.truetype( path.join('font', 'sarasa-fixed-sc-regular.ttf'), 15) # 页码字体 FONT_ICO = ImageFont.truetype( path.join('font', 'sarasa-fixed-sc-regular.ttf'), 22) # 图标区域的字体 FONT_CLOCK = ImageFont.truetype(path.join('font', 'hartland.ttf'), 50) # 时钟字体 FONT_DATE = ImageFont.truetype(path.join('font', 'hartland.ttf'), 23) # 日期字体 FONT_TIPS = ImageFont.truetype( path.join('font', 'sarasa-fixed-sc-regular.ttf'), 13) # 日期下方文字的字体 def generate_2fa_img(data: List[Tuple[str, str]], page: Tuple[int, int] = (1, 1), active: bool = True, tips: List[str] = None) -> Image: def draw_icon() -> Image: '''绘制图标''' img = Image.new('L', (ICON_W, SCREEN_H), 0xff) w, h = FONT_ICO.getsize('▲') draw = ImageDraw.Draw(img) ICO_TXT = ['▲', '▼', '●', '■'] if active else ['△', '▽', '○', '□'] for i in range(4): x, y = (ICON_W-w)/2+1, i*44+(44-h)/2 draw.text((x, y), ICO_TXT[i], font=FONT_ICO, fill=0x00) return img def draw_auth() -> Image: '''绘制令牌''' def draw_auth_single(name: str, code: str) -> Image: '''绘制单个令牌''' # 计算尺寸 w1, h1 = FONT_CODE.getsize(code) w2, h2 = FONT_TAG.getsize(name[:3]) W2, H2 = CODE_H, 18 W1, H1 = CODE_W-H2, CODE_H # 画令牌 im_code = Image.new('L', (CODE_W, CODE_H), 0xff) draw_code = ImageDraw.Draw(im_code) draw_code.text(((W1-w1)/2, (H1-h1)/2), code, font=FONT_CODE, fill=0x00) # 画名称 im_name = Image.new('L', (W2, H2), 0xff) draw_name = ImageDraw.Draw(im_name) draw_name.text(((W2-w2)/2, (H2-h2)/2-2), name[:3], font=FONT_TAG, fill=0x00) draw_name.line(((0, 0), (W2, 0)), fill=0x00, width=1) im_name = im_name.rotate(angle=90, expand=True) im_code.paste(im_name, (W1, 0)) draw_code.rectangle((0, 0, CODE_W-1, CODE_H), outline=0) return im_code def draw_page_info() -> Image: '''绘制页码''' PAGE_H = SCREEN_H-6*CODE_H+2 im_page = Image.new('L', (CODE_W, PAGE_H), 0xff) draw = ImageDraw.Draw(im_page) draw.rectangle((0, 0, CODE_W-1, PAGE_H), outline=0) curr, total = page curr -= 1 if total < 1: total = 1 if curr > total: curr = 0 pages = ['-'] * total if active: pages[curr] = '≡' else: pages[curr] = '≡' page_str = ' '.join(pages) wp, hp = FONT_PAGE.getsize(page_str) draw.text(((CODE_W-wp)/2, (PAGE_H-hp)/2-2), page_str, font=FONT_PAGE, fill=0x00) return im_page img = Image.new('L', (CODE_W, SCREEN_H), 0xff) y = -1 for name, code in data[:6]: img_code = draw_auth_single(name, code) img.paste(img_code, (0, y)) y += img_code.height img_page = draw_page_info() img.paste(img_page, (0, y)) return img def draw_clock() -> Image: '''绘制时钟''' img = Image.new('L', (CLOCK_W, CLOCK_H), 0xff) draw = ImageDraw.Draw(img) time_str = time.strftime("%H:%M", time.localtime()) date_str = time.strftime("%Y-%m-%d", time.localtime()) # 画时钟 wc, hc = FONT_CLOCK.getsize(time_str) wd, hd = FONT_DATE.getsize(date_str) draw.text(((CLOCK_W-wc)/2+3, 10), time_str, font=FONT_CLOCK, fill=0x00) draw.text(((CLOCK_W-wd)/2+3, 65), date_str, font=FONT_DATE, fill=0x00) for i, tip in enumerate(tips, 0): wt, ht = FONT_TIPS.getsize(tip) draw.text(((CLOCK_W-wt)/2, 110+(ht+5)*i), tip, font=FONT_TIPS, fill=0x00) return img if not active: data = [('---', '-----')] * 6 if len(data) < 6: data += [('---', '-----')] * 6 if not tips: tips = ['By Chr_', 'Ver 0.01', 'chrxw.com'] full_img = Image.new('L', (SCREEN_W, SCREEN_H), 0xff) img_auth = draw_auth() full_img.paste(img_auth, (ICON_W, 0)) img_icon = draw_icon() full_img.paste(img_icon, (0, 0)) img_clock = draw_clock() full_img.paste(img_clock, (ICON_W+CODE_W, 0)) with open('o.png', 'wb') as f: full_img.save(f, 'png') return full_img PIL绘图简介 创建图片('L'代表灰度模式，0xff表示默认颜色是白色) img = Image.new('L', (width, height), 0xff) 图片拼贴(把img_icon粘贴到img(30,0)的位置) img.paste(img_icon, (30, 0)) 创建画笔对象 draw = ImageDraw.Draw(img) 绘制矩形(左上角为(x1,y1)，右下角为(x2,y2)) draw.rectangle((x1, y1, x2, y2), outline=0) 绘制直线(端点为(x1,y1)和(x2,y2)，0x00表示线条颜色为黑色，宽度为1) draw_name.line(((0, 0), (W2, 0)), fill=0x00, width=1) 创建字体对象('font/CascadiaMono.ttf'是字体路径，12是字号) font = ImageFont.truetype('font/CascadiaMono.ttf'), 12) 获取文字的尺寸(获取用指定字体把'text'显示出来的尺寸，高为h，宽为w) w, h = font.getsize('text') 绘制文本(把'text'用指定字体显示在(20,10)的位置，0x00表示字体颜色是黑色) draw.text((20, 10), 'text', font=font, fill=0x00) 按键响应 原理 树莓派有2种引脚编号方式，BCM和BOARD，两者的区别只是对引脚的标记不同，使用GPIO前申明一下引脚的编号方式即可，使用命令gpio readall可以查看所有引脚的编号、状态、电平状态 类似于51单片机的中断系统，树莓派也可以设定中断，不过先要把指定引脚设定为输入模式，设定完后，再创建一个“监听器”，当指定引脚的电平从高电平变为低电平的时候，就会自动调用指定函数，参数为引脚号。 我买的屏幕左边4个按键使用BCM编码的引脚编号为【5,6,13,19】，前两个用来控制翻页，第三个用来切换时钟下方显示的内容，第四个用来控制令牌的显示/隐藏。 实现 i> 这里只列出了事件响应的部分，具体功能的实现请参考obj.py import RPi.GPIO as GPIO from obj import Pi_2FA # 使用的引脚序号 KEYS = [5, 6, 13, 19] pi_2fa = Pi_2FA() # 按键响应回调函数 def btn_callback(key): if pi_2fa.Ready: if key == KEYS[0]: pi_2fa.page_down() elif key == KEYS[1]: pi_2fa.page_up() elif key ==KEYS[2]: pi_2fa.switch_mode() elif key ==KEYS[3]: pi_2fa.none_mode() else: print(key) return # 设置引脚编号方式为BCM GPIO.setmode(GPIO.BCM) for key in KEYS: # 设定指定引脚为输入模式 GPIO.setup(key, GPIO.IN, pull_up_down=GPIO.PUD_UP) # 为指定引脚添加事件响应函数 GPIO.add_event_detect(key, GPIO.RISING, callback=btn_callback, bouncetime=200)
前言 最近开始关注Steam游戏的下架情报，付费游戏下架还可能买得到激活码，免费游戏下架绝大多数情况都没有办法收藏了，于是就有了这个能够自动添加免费游戏的项目 使用方法 链接 本项目链接：Github ASF配置 添加免费游戏通过ASF实现，所有首先需要配置ASF，打开ASF的IPC功能 只要在ASF.json中，加一条"IPC": true,即可 脚本安装 & 配置 首先克隆仓库 git clone https://github.com/chr233/free_game_hunter cd free_game_hunter pip3 install -r requirements.txt mv example.config.toml config.toml 在本地会存在一个data.db，会缓存机器人拥有的游戏和steam商店上的免费游戏，然后会保存统计数据，请不要自行修改data.db 然后修改config.toml，设置机器人名称即可 [bot] # 机器人设置 # 机器人名称,不能设置为asf bot_name = '1' # 多个机器人可以设置写成这样 # bot_name = ['1','2','3'] [asf] # asf设置 # ipc地址 ipc_addr = 'http://localhost:1242' # ipc连接密码,没有密码的话留空 ipc_password = '' [db] # 数据库配置 # 数据库名称 db_name = 'data.db' 运行脚本 使用Python3运行 python3 run.py 自动运行 我使用crontab实现定时执行（因为Steam限制，一个账号每小时最多添加50个Sub，所以一小时执行一次就行了） # 每个小时执行一次 0 * * * * /home/xxx/free_game_hunter/start.sh > /dev/null 2>&1 & start.sh内容： # 需要先切换到工作目录才可以，路径自行修改 cd /home/xxx/free_game_hunter python3 run.py 运行效果