+
\ No newline at end of file
diff --git a/mixly/mixly-sw/templete/loader-div.html b/mixly/mixly-sw/templete/loader-div.html
new file mode 100644
index 00000000..0207e429
--- /dev/null
+++ b/mixly/mixly-sw/templete/loader-div.html
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/mixly/mixly-sw/templete/progress-bar-div.html b/mixly/mixly-sw/templete/progress-bar-div.html
new file mode 100644
index 00000000..b2931511
--- /dev/null
+++ b/mixly/mixly-sw/templete/progress-bar-div.html
@@ -0,0 +1,19 @@
+
+
+
+
{{d.boardType}}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mixly/mixly-sw/templete/setting-div.html b/mixly/mixly-sw/templete/setting-div.html
new file mode 100644
index 00000000..eb78a915
--- /dev/null
+++ b/mixly/mixly-sw/templete/setting-div.html
@@ -0,0 +1,405 @@
+
+
\ No newline at end of file
diff --git a/mixly/static-server/api.js b/mixly/static-server/api.js
new file mode 100644
index 00000000..468c8777
--- /dev/null
+++ b/mixly/static-server/api.js
@@ -0,0 +1,276 @@
+const fs = require('fs');
+const path = require('path');
+const express = require('express');
+const axios = require('axios');
+const AdmZip = require('adm-zip');
+
+
+class AsyncAdmZip {
+ constructor(zipPath) {
+ this.zipPath = zipPath;
+ this.zip = new AdmZip(zipPath);
+ }
+
+ // 异步解压到目录
+ async extractAllTo(targetPath, overwrite = true) {
+ return new Promise((resolve, reject) => {
+ try {
+ // 确保目标目录存在
+ fs.promises.mkdir(targetPath, { recursive: true })
+ .then(() => {
+ this.zip.extractAllTo(targetPath, overwrite);
+ resolve({
+ success: true,
+ targetPath,
+ fileCount: this.zip.getEntries().length
+ });
+ })
+ .catch(reject);
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ // 异步解压单个文件
+ async extractEntry(entryName, targetPath, overwrite = true) {
+ return new Promise((resolve, reject) => {
+ try {
+ const entry = this.zip.getEntry(entryName);
+ if (!entry) {
+ reject(new Error(`条目不存在: ${entryName}`));
+ return;
+ }
+
+ if (entry.isDirectory) {
+ reject(new Error(`条目是目录: ${entryName}`));
+ return;
+ }
+
+ // 确保目标目录存在
+ const targetDir = path.dirname(targetPath);
+ fs.promises.mkdir(targetDir, { recursive: true })
+ .then(() => {
+ this.zip.extractEntryTo(entry, targetDir, false, overwrite);
+ resolve({
+ success: true,
+ entryName,
+ targetPath
+ });
+ })
+ .catch(reject);
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ // 异步获取zip文件信息
+ async getZipInfo() {
+ return new Promise((resolve) => {
+ const entries = this.zip.getEntries();
+ const info = {
+ fileCount: entries.length,
+ totalSize: entries.reduce((sum, entry) => sum + entry.header.size, 0),
+ entries: entries.map(entry => ({
+ name: entry.entryName,
+ size: entry.header.size,
+ isDirectory: entry.isDirectory,
+ compressedSize: entry.header.compressedSize
+ }))
+ };
+ resolve(info);
+ });
+ }
+}
+
+
+const router = express.Router();
+
+
+const TEMP_FOLDER_PATH = path.resolve(__dirname, '../temp');;
+const VERSION_FILE = path.resolve(__dirname, '../version.json');
+
+function getLocalVersion() {
+ try {
+ if (fs.existsSync(VERSION_FILE)) {
+ const data = fs.readFileSync(VERSION_FILE, 'utf8');
+ return data;
+ }
+ } catch (error) {
+ console.error('读取版本文件失败:', error);
+ }
+ return '2025.09.06';
+}
+
+function saveVersionInfo(version) {
+ fs.writeFileSync(VERSION_FILE, version);
+}
+
+async function getCloudVersion() {
+ try {
+ const response = await axios.get('http://update.mixly.cn/index.php');
+ return response.data.mixly.all;
+ } catch (error) {
+ console.error('获取云端版本信息失败:', error);
+ return {
+ file: 'mixly.zip',
+ version: '2025.09.06'
+ };
+ }
+}
+
+async function checkUpdate() {
+ try {
+ const localVersion = getLocalVersion();
+ const cloudVersions = await getCloudVersion();
+ const cloudVersion = cloudVersions['version'];
+ const cloudFile = 'http://update.mixly.cn/download.php?file=' + cloudVersions['file']
+ return {
+ needsUpdate: localVersion !== cloudVersion,
+ localVersion: localVersion,
+ cloudVersion: cloudVersion,
+ cloudFile: cloudFile,
+ error: ''
+ };
+ } catch (error) {
+ return {
+ needsUpdate: false,
+ localVersion: '',
+ cloudVersion: '',
+ cloudFile: '',
+ error: error.message
+ }
+ }
+}
+
+// 检查更新接口
+router.post('/check-update', async (req, res) => {
+ const updateInfo = await checkUpdate();
+ res.json(updateInfo);
+});
+
+// 下载进度返回
+function deleteFolderRecursive(dirPath) {
+ if (fs.existsSync(dirPath)) {
+ fs.readdirSync(dirPath).forEach(file => {
+ const curPath = path.join(dirPath, file);
+ if (fs.lstatSync(curPath).isDirectory()) {
+ deleteFolderRecursive(curPath);
+ } else {
+ fs.unlinkSync(curPath);
+ }
+ });
+ fs.rmdirSync(dirPath);
+ }
+}
+
+router.get('/download', async (req, res) => {
+ try {
+ const { url, cloudVersion } = req.query;
+
+ // 清理临时文件夹
+ if (fs.existsSync(TEMP_FOLDER_PATH)) {
+ deleteFolderRecursive(TEMP_FOLDER_PATH);
+ }
+ fs.mkdirSync(TEMP_FOLDER_PATH, { recursive: true });
+
+ const filePath = path.resolve(TEMP_FOLDER_PATH, 'mixly.zip');
+ const fileStream = fs.createWriteStream(filePath);
+
+ // 设置 SSE 响应头
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.flushHeaders();
+
+ // 发起下载请求 - 添加 NW.js 特定配置
+ const response = await axios({
+ method: 'GET',
+ url: url,
+ responseType: 'stream',
+ timeout: 60000,
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ },
+ adapter: require('axios/lib/adapters/http')
+ });
+
+ const totalSize = parseInt(response.headers['content-length'], 10) || 0;
+
+ let downloadedSize = 0;
+ let lastProgress = 0;
+
+ // 发送进度信息
+ const sendProgress = (progress) => {
+ if (progress !== lastProgress) {
+ const data = JSON.stringify({ type: 'progress', progress });
+ res.write(`data: ${data}\n\n`);
+ lastProgress = progress;
+ }
+ };
+
+ // 管道流处理
+ response.data.pipe(fileStream);
+
+ // 进度监控
+ response.data.on('data', (chunk) => {
+ downloadedSize += chunk.length;
+ if (totalSize > 0) {
+ const progress = Math.round((downloadedSize / totalSize) * 100);
+ sendProgress(progress);
+ }
+ });
+
+ // 文件流完成
+ fileStream.on('finish', async () => {
+ console.log('文件下载完成,开始解压');
+
+ // 发送解压信息
+ res.write(`data: ${JSON.stringify({ type: 'unzip' })}\n\n`);
+
+ try {
+ const asyncZip = new AsyncAdmZip(filePath);
+ await asyncZip.extractAllTo(path.resolve(__dirname, '../'));
+
+ // 保存版本信息
+ saveVersionInfo(cloudVersion);
+
+ // 发送完成信息
+ res.write(`data: ${JSON.stringify({ type: 'complete', version: cloudVersion })}\n\n`);
+
+ // 清理临时文件
+ if (fs.existsSync(TEMP_FOLDER_PATH)) {
+ deleteFolderRecursive(TEMP_FOLDER_PATH);
+ }
+
+ res.end();
+ } catch (error) {
+ console.error('解压失败:', error);
+ res.write(`data: ${JSON.stringify({ type: 'error', message: '解压失败' })}\n\n`);
+ res.end();
+ }
+ });
+
+ // 错误处理
+ response.data.on('error', (error) => {
+ console.error('下载流错误:', error);
+ res.write(`data: ${JSON.stringify({ type: 'error', message: '下载流错误' })}\n\n`);
+ res.end();
+ });
+
+ fileStream.on('error', (error) => {
+ console.error('文件流错误:', error);
+ res.write(`data: ${JSON.stringify({ type: 'error', message: '文件保存错误' })}\n\n`);
+ res.end();
+ });
+
+ } catch (error) {
+ console.error('下载过程错误:', error);
+ res.write(`data: ${JSON.stringify({ type: 'error', message: '下载失败: ' + error.message })}\n\n`);
+ res.end();
+ }
+});
+
+
+module.exports = router;
diff --git a/mixly/static-server/certs/server.crt b/mixly/static-server/certs/server.crt
new file mode 100644
index 00000000..d4d0137b
--- /dev/null
+++ b/mixly/static-server/certs/server.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDNTCCAh0CFGQSgGkvHiuKgOSMVVjT1dFX1Df3MA0GCSqGSIb3DQEBCwUAMFcx
+CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5n
+MQ4wDAYDVQQKDAVNaXhseTEUMBIGA1UEAwwLMTkyLjE2OC4xLjEwHhcNMjIwODE5
+MDg1ODA4WhcNMjMwODE5MDg1ODA4WjBXMQswCQYDVQQGEwJDTjEQMA4GA1UECAwH
+QmVpamluZzEQMA4GA1UEBwwHQmVpamluZzEOMAwGA1UECgwFTWl4bHkxFDASBgNV
+BAMMCzE5Mi4xNjguMS4xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+yxvOxepem9zCBTGZ2aMjba73xkVgljGuEhbPMgRS8l763ril2rapfGoiBVEGOkTv
+RY6YFP6yNtX4E8SXuePcvS75g+XUVgUYLmXJ+2DZfNL/RLnTRciiGR/n53BEbE9R
+c81I/yYbCmWJH63F0Zn4SOghzP9AyBSTkVVHt2onm4NNCWEhsmAFlHOsiNQeOd4r
+Ji5k9KFjhstKAtlDr/HLjTTew15y2GVhusjko52HYM9zYrNOPrOHzuQza4cxln37
+FQUZudJx1TttWJ4u8z8ycAHNJ+Nj9yUzIo55xe/R+/qpF+ADxVMbm+Dpx2wIp6iG
+9WZSBWXu+6tfTuUjDsDUNwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQC4gqiSyqpb
+8Z8p+VqF3ezJXMBtTyOnBsWXAO8UUJ8pEIrTVWBj+MQfbrYwX54omApNra8CGC+g
+yGa84QNVRgor71sV+rpwXu/+2vc1eExJTjsias4TSgmzJYGH2NLcNxAoru7KQjzv
+Inrn88QMcDdhIkKTjxpu1uz75p62oMD6Iv58n+4iBLxfW8GPQ5U8BKOVps+DWTOI
+lDMtuc4ZySLDUNn8tHsKSLL41kupmkqAd9G12bqR+BOs06Nf/IeVuYGtZDw5gZxg
+DpKb826ZTsI7ED34qhG+4mol6py8wM3UeDJ8QX9EANwwDA7DJ1boC8QIxAk5fgA1
+rzyUgc30MMx0
+-----END CERTIFICATE-----
diff --git a/mixly/static-server/certs/server.csr b/mixly/static-server/certs/server.csr
new file mode 100644
index 00000000..38784014
--- /dev/null
+++ b/mixly/static-server/certs/server.csr
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICnDCCAYQCAQAwVzELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAO
+BgNVBAcMB0JlaWppbmcxDjAMBgNVBAoMBU1peGx5MRQwEgYDVQQDDAsxOTIuMTY4
+LjEuMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMsbzsXqXpvcwgUx
+mdmjI22u98ZFYJYxrhIWzzIEUvJe+t64pdq2qXxqIgVRBjpE70WOmBT+sjbV+BPE
+l7nj3L0u+YPl1FYFGC5lyftg2XzS/0S500XIohkf5+dwRGxPUXPNSP8mGwpliR+t
+xdGZ+EjoIcz/QMgUk5FVR7dqJ5uDTQlhIbJgBZRzrIjUHjneKyYuZPShY4bLSgLZ
+Q6/xy4003sNecthlYbrI5KOdh2DPc2KzTj6zh87kM2uHMZZ9+xUFGbnScdU7bVie
+LvM/MnABzSfjY/clMyKOecXv0fv6qRfgA8VTG5vg6cdsCKeohvVmUgVl7vurX07l
+Iw7A1DcCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBiNeSZNC0CFlxd1lxhu0cb
++w5T2ikskgKUyxe/5ZDVFfzXaDw/JrE/9sdsnecl/t0Wbsir5lRLH0LOe0XyusuD
+DcVeOEVUjVWTSgtR5Te9pjKHBphyu0fdHQG6LKNa6UGThc6aaKyjkxRv8dFIKkjl
+FHceesd51pBKRdIhmFXg9kx+9iJVKgLL92U8uKgsjZBB4ZzYaUD9RLaPxaK52gOf
+y6TswblJbCUxEl8vCIyB1v5rGngJ7kCjuEk7lNDSRI58GBkEzFQYLHAi7C/BOLDY
+DDpJaPr9crfmD3UboAqgLdi5S8W2BN59VcLUtp4/2bgfcvYQFUahKvyTymf6OmU9
+-----END CERTIFICATE REQUEST-----
diff --git a/mixly/static-server/certs/server.key b/mixly/static-server/certs/server.key
new file mode 100644
index 00000000..7223f9e2
--- /dev/null
+++ b/mixly/static-server/certs/server.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAyxvOxepem9zCBTGZ2aMjba73xkVgljGuEhbPMgRS8l763ril
+2rapfGoiBVEGOkTvRY6YFP6yNtX4E8SXuePcvS75g+XUVgUYLmXJ+2DZfNL/RLnT
+RciiGR/n53BEbE9Rc81I/yYbCmWJH63F0Zn4SOghzP9AyBSTkVVHt2onm4NNCWEh
+smAFlHOsiNQeOd4rJi5k9KFjhstKAtlDr/HLjTTew15y2GVhusjko52HYM9zYrNO
+PrOHzuQza4cxln37FQUZudJx1TttWJ4u8z8ycAHNJ+Nj9yUzIo55xe/R+/qpF+AD
+xVMbm+Dpx2wIp6iG9WZSBWXu+6tfTuUjDsDUNwIDAQABAoIBABOlPevQzpPe13lv
+IcV2TR/304l+/meoqIChaisZVfiRjUxrqccs8dnR3jaLbsHGFyqwLy+grxY0vgkT
+c+WMD7bQy1uhqFclqQAb4lyJMqArPHumSbQvQtaRSnoNVuDvDx7XVV8wjV8FES1a
+Po8WiHhs05AjhF2V9+wPxp8MCoa1EWMh+A9gkswvbaDrh+YTC3qori2H9gvt4DPp
+feb1ofiMLSM2HzUtJgw+kp5SvWtYvg001f8pO7A2KIV0MoE38tXuaebxta0YHql+
+BuICWCe2M7RZau2JHZM51tPQ2/3l6ya2YSwdPPRCUQqpJ8uFS1M1QnjZKFlAEXY/
+KU4lmskCgYEA7J9EyIMqfI6pbGLeLnbf8aN/6C5ldOvEYZThlcjDpOMveajmylYs
+P0SFsK05MaFf5PeM0U2WHfzx8bLyLUvIpICvJ5w9q9vR/ZOxHCBd87dMqjVDFgXm
+iUz+m87D7wVNFKzZF+lnqdqOpouuBfMlJqbCygmcw4ISEwntOnDRsy0CgYEA273v
+eRMTXM+VmJ4ZcTYXpRFGOFqP8yITyQ8dm7Yr+RpJAxLS/fN9DZEsl8xWNevFmbp4
+isDtKNKjmQIsvpxt2s30dpqg53YWnfHjFVN/JLYbxzYavYcciJ/Apl6RyRYukTPC
+T99gUMfu+/7LHgS2ytzZ0kmVBmKaXAwUzU3lE3MCgYAYm/frYrjoe23jd+TjsDla
+SEblPu4OWvbxrypHCbpPS9GENazLHms7qUS+O0XXg5EVnylmG0uhks0W9iV50Ift
+k/SjifxgA1yzosioxDUBQ+8VRLTVdYekf/169uYp1cNOgyuQ8RV29OQhLiXLOJ6E
+hpN7r8Q+ESkQEdg6W8FzgQKBgHKbZHvsVAvzBJ39z105jil8kfgwW6W+Xz1dEd81
+q0eXyv68Yakrxkw+LFjbrRcgagYcuGP97XN+MO9LsBSWN8GH63m0ejleYLtt/jcQ
+Pl7iUCidcmLpRhuH3o2nAzgyxoTazvyjj3NyY5WwtTVp1gCGIWFJGV2kLcfWUT8m
+4lQ7AoGBAM5R5jI9n8ndNhGpoIisu8YCeowS5QGDJznBN932NwenUgiZw4suv71r
+v5FAdjIMKwSQcjaZoPPqqTuXnTIo1mXQpiMitAAXeb1HmJnPvZgSk5RI3OnrOZDE
+CClJzjTTEprhaw9LR7uqWUp12f1Z+lAq9kQFAeYBarpqIplH6/Gi
+-----END RSA PRIVATE KEY-----
diff --git a/mixly/static-server/server.js b/mixly/static-server/server.js
new file mode 100644
index 00000000..524be129
--- /dev/null
+++ b/mixly/static-server/server.js
@@ -0,0 +1,31 @@
+const fs = require('fs')
+const StaticServer = require('./static-server.js');
+const SSLStaticServer = require('./static-sslserver.js');
+
+
+function deleteFile(filePath) {
+ try {
+ if (!fs.existsSync(filePath)) {
+ return;
+ }
+ const stats = fs.statSync(filePath);
+ if (stats.isFile()) {
+ fs.unlinkSync(filePath);
+ console.log('File deleted successfully.');
+ }
+ } catch (err) {
+ console.error('Error deleting file:', err);
+ }
+}
+
+const init = () => {
+ StaticServer.run('7000');
+ SSLStaticServer.run('8000');
+}
+
+if (!module.parent) {
+ deleteFile('./nw_cache/Default/Preferences');
+ init();
+} else {
+ module.exports = init;
+}
diff --git a/mixly/static-server/static-server.js b/mixly/static-server/static-server.js
new file mode 100644
index 00000000..de113bee
--- /dev/null
+++ b/mixly/static-server/static-server.js
@@ -0,0 +1,22 @@
+const http = require('http');
+const express = require('express');
+const path = require('path');
+const apiRoutes = require('./api.js');
+
+const StaticServer = {};
+
+StaticServer.run = (port) => {
+ const app = express();
+ app.use(express.static(path.resolve(__dirname, '../')));
+ app.use('/api/', apiRoutes);
+ const httpServer = http.createServer(app);
+ httpServer.listen(port);
+ console.log('Static服务器正在运行 [端口 - ' + port + ', http]...');
+ console.log('访问地址:http://127.0.0.1:' + port);
+ StaticServer.server = httpServer;
+ StaticServer.app = app;
+ StaticServer.port = port;
+ StaticServer.protocol = 'http';
+}
+
+module.exports = StaticServer;
\ No newline at end of file
diff --git a/mixly/static-server/static-sslserver.js b/mixly/static-server/static-sslserver.js
new file mode 100644
index 00000000..2e2fb68b
--- /dev/null
+++ b/mixly/static-server/static-sslserver.js
@@ -0,0 +1,27 @@
+const https = require('https');
+const express = require('express');
+const fs = require('fs');
+const path = require('path');
+
+const SSLStaticServer = {};
+
+SSLStaticServer.run = (port) => {
+ const KEY_PATH = path.resolve(__dirname, './certs/server.key');
+ const CRT_PATH = path.resolve(__dirname, './certs/server.crt');
+ const options = {
+ key: fs.readFileSync(KEY_PATH),
+ cert: fs.readFileSync(CRT_PATH)
+ };
+ const app = express();
+ app.use(express.static(path.resolve(__dirname, '../')));
+ const httpsServer = https.createServer(options, app);
+ httpsServer.listen(port);
+ console.log('Static服务器正在运行 [端口 - ' + port + ', https]...');
+ console.log('访问地址:https://127.0.0.1:' + port);
+ SSLStaticServer.server = httpsServer;
+ SSLStaticServer.app = app;
+ SSLStaticServer.port = port;
+ SSLStaticServer.protocol = 'https';
+}
+
+module.exports = SSLStaticServer;
diff --git a/mixly/tools/python/ampy/__init__.py b/mixly/tools/python/ampy/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mixly/tools/python/ampy/cli.py b/mixly/tools/python/ampy/cli.py
new file mode 100644
index 00000000..6dec35f7
--- /dev/null
+++ b/mixly/tools/python/ampy/cli.py
@@ -0,0 +1,552 @@
+# Adafruit MicroPython Tool - Command Line Interface
+# Author: Tony DiCola
+# Copyright (c) 2016 Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+from __future__ import print_function
+import os
+import platform
+import posixpath
+import re
+import serial.serialutil
+import binascii
+import click
+import dotenv
+
+import sys
+
+# Load AMPY_PORT et al from .ampy file
+# Performed here because we need to beat click's decorators.
+config = dotenv.find_dotenv(filename=".ampy", usecwd=True)
+if config:
+ dotenv.load_dotenv(dotenv_path=config)
+
+import ampy.files as files
+import ampy.pyboard as pyboard
+
+
+_board = None
+
+
+def windows_full_port_name(portname):
+ # Helper function to generate proper Windows COM port paths. Apparently
+ # Windows requires COM ports above 9 to have a special path, where ports below
+ # 9 are just referred to by COM1, COM2, etc. (wacky!) See this post for
+ # more info and where this code came from:
+ # http://eli.thegreenplace.net/2009/07/31/listing-all-serial-ports-on-windows-with-python/
+ m = re.match("^COM(\d+)$", portname)
+ if m and int(m.group(1)) < 10:
+ return portname
+ else:
+ return "\\\\.\\{0}".format(portname)
+
+
+@click.group()
+@click.option(
+ "--port",
+ "-p",
+ envvar="AMPY_PORT",
+ required=True,
+ type=click.STRING,
+ help="Name of serial port for connected board. Can optionally specify with AMPY_PORT environment variable.",
+ metavar="PORT",
+)
+@click.option(
+ "--baud",
+ "-b",
+ envvar="AMPY_BAUD",
+ default=115200,
+ type=click.INT,
+ help="Baud rate for the serial connection (default 115200). Can optionally specify with AMPY_BAUD environment variable.",
+ metavar="BAUD",
+)
+@click.option(
+ "--delay",
+ "-d",
+ envvar="AMPY_DELAY",
+ default=0,
+ type=click.FLOAT,
+ help="Delay in seconds before entering RAW MODE (default 0). Can optionally specify with AMPY_DELAY environment variable.",
+ metavar="DELAY",
+)
+@click.option(
+ "--reset",
+ "-r",
+ envvar="AMPY_RESET",
+ default="{}",
+ type=click.STRING,
+ help="default={}",
+ metavar="RESET",
+)
+@click.option(
+ "--empty",
+ "-e",
+ envvar="AMPY_EMPTY",
+ default="main.py",
+ type=click.STRING,
+ help="default=main.py",
+ metavar="EMPTY",
+)
+@click.option(
+ "--info",
+ "-i",
+ envvar="AMPY_INFO",
+ default=True,
+ type=click.BOOL,
+ help="default=True",
+ metavar="INFO",
+)
+@click.version_option()
+def cli(port, baud, delay, reset="{}", empty="main.py", info=True):
+ """ampy - Adafruit MicroPython Tool
+
+ Ampy is a tool to control MicroPython boards over a serial connection. Using
+ ampy you can manipulate files on the board's internal filesystem and even run
+ scripts.
+ """
+ global _board
+ # On Windows fix the COM port path name for ports above 9 (see comment in
+ # windows_full_port_name function).
+ #sys.stdout.write(reset + "\n")
+ #sys.stdout.flush()
+ if platform.system() == "Windows":
+ port = windows_full_port_name(port)
+ _board = pyboard.Pyboard(port, baudrate=baud, rawdelay=delay, boardreset=reset, file_empty=empty, info=info)
+
+
+@cli.command()
+@click.argument("remote_file")
+@click.argument("local_file", type=click.File("wb"), required=False)
+def get(remote_file, local_file):
+ """
+ Retrieve a file from the board.
+
+ Get will download a file from the board and print its contents or save it
+ locally. You must pass at least one argument which is the path to the file
+ to download from the board. If you don't specify a second argument then
+ the file contents will be printed to standard output. However if you pass
+ a file name as the second argument then the contents of the downloaded file
+ will be saved to that file (overwriting anything inside it!).
+
+ For example to retrieve the boot.py and print it out run:
+
+ ampy --port /board/serial/port get boot.py
+
+ Or to get main.py and save it as main.py locally run:
+
+ ampy --port /board/serial/port get main.py main.py
+ """
+ # Get the file contents.
+ board_files = files.Files(_board)
+ contents = board_files.get(remote_file)
+ # Print the file out if no local file was provided, otherwise save it.
+ if local_file is None:
+ contents = str(contents)[2:-1]
+ print(contents, end='')
+ else:
+ value = binascii.unhexlify(contents)
+ local_file.write(value.decode("utf-8"))
+
+
+@cli.command()
+@click.option(
+ "--exists-okay", is_flag=True, help="Ignore if the directory already exists."
+)
+@click.argument("directory")
+def mkdir(directory, exists_okay):
+ """
+ Create a directory on the board.
+
+ Mkdir will create the specified directory on the board. One argument is
+ required, the full path of the directory to create.
+
+ Note that you cannot recursively create a hierarchy of directories with one
+ mkdir command, instead you must create each parent directory with separate
+ mkdir command calls.
+
+ For example to make a directory under the root called 'code':
+
+ ampy --port /board/serial/port mkdir /code
+ """
+ # Run the mkdir command.
+ board_files = files.Files(_board)
+ board_files.mkdir(directory, exists_okay=exists_okay)
+
+
+@cli.command()
+@click.argument("file")
+def mkfile(file):
+ board_files = files.Files(_board)
+ board_files.mkfile(file)
+
+
+@cli.command()
+@click.argument("directory", default="/")
+@click.option(
+ "--long_format",
+ "-l",
+ is_flag=True,
+ help="Print long format info including size of files. Note the size of directories is not supported and will show 0 values.",
+)
+@click.option(
+ "--recursive",
+ "-r",
+ is_flag=True,
+ help="recursively list all files and (empty) directories.",
+)
+def ls(directory, long_format, recursive):
+ """List contents of a directory on the board.
+
+ Can pass an optional argument which is the path to the directory. The
+ default is to list the contents of the root, /, path.
+
+ For example to list the contents of the root run:
+
+ ampy --port /board/serial/port ls
+
+ Or to list the contents of the /foo/bar directory on the board run:
+
+ ampy --port /board/serial/port ls /foo/bar
+
+ Add the -l or --long_format flag to print the size of files (however note
+ MicroPython does not calculate the size of folders and will show 0 bytes):
+
+ ampy --port /board/serial/port ls -l /foo/bar
+ """
+ # List each file/directory on a separate line.
+ board_files = files.Files(_board)
+ for f in board_files.ls(directory, long_format=long_format, recursive=recursive):
+ print(f)
+
+
+@cli.command()
+@click.argument("local", type=click.Path(exists=True))
+@click.argument("remote", required=False)
+def put(local, remote):
+ """Put a file or folder and its contents on the board.
+
+ Put will upload a local file or folder to the board. If the file already
+ exists on the board it will be overwritten with no warning! You must pass
+ at least one argument which is the path to the local file/folder to
+ upload. If the item to upload is a folder then it will be copied to the
+ board recursively with its entire child structure. You can pass a second
+ optional argument which is the path and name of the file/folder to put to
+ on the connected board.
+
+ For example to upload a main.py from the current directory to the board's
+ root run:
+
+ ampy --port /board/serial/port put main.py
+
+ Or to upload a board_boot.py from a ./foo subdirectory and save it as boot.py
+ in the board's root run:
+
+ ampy --port /board/serial/port put ./foo/board_boot.py boot.py
+
+ To upload a local folder adafruit_library and all of its child files/folders
+ as an item under the board's root run:
+
+ ampy --port /board/serial/port put adafruit_library
+
+ Or to put a local folder adafruit_library on the board under the path
+ /lib/adafruit_library on the board run:
+
+ ampy --port /board/serial/port put adafruit_library /lib/adafruit_library
+ """
+ # Use the local filename if no remote filename is provided.
+
+ # Check if path is a folder and do recursive copy of everything inside it.
+ # Otherwise it's a file and should simply be copied over.
+
+ if os.path.isdir(local):
+ if remote is None:
+ remote = ""
+ # Directory copy, create the directory and walk all children to copy
+ # over the files.
+ #print("true")
+ #print(remote)
+ board_files = files.Files(_board)
+ board_files._pyboard.enter_raw_repl()
+ file_empty = board_files._pyboard.file_empty
+ # sys.stdout.write("Empty ./{}...\n".format(file_empty))
+ # sys.stdout.flush()
+ # board_files.put('./{}'.format(file_empty), '', False, False)
+ # sys.stdout.write("Empty ./{} Done!\n".format(file_empty))
+ # sys.stdout.flush()
+ files_info = board_files.getFilesInfo('')
+ files_dict = {}
+ files_info_len = len(files_info)
+ for i in range(0, files_info_len, 1):
+ if files_info[i][0][0] == '/':
+ files_info[i][0] = files_info[i][0][1 : len(files_info[i][0])]
+ files_dict[files_info[i][0]] = files_info[i][1]
+ # sys.stdout.write(str(files_dict))
+ # sys.stdout.flush()
+ for parent, child_dirs, child_files in os.walk(local, followlinks=True):
+ # Create board filesystem absolute path to parent directory.
+
+ remote_parent = posixpath.normpath(
+ posixpath.join(remote, os.path.relpath(parent, local))
+ )
+ #print(remote_parent)
+ '''
+ try:
+ # Create remote parent directory.
+ print(remote_parent)
+ board_files.mkdir(remote_parent)
+ except files.DirectoryExistsError:
+ # Ignore errors for directories that already exist.
+ pass
+ # Loop through all the files and put them on the board too.
+ '''
+
+
+ file_name_list = []
+ data_list = []
+ for filename in child_files:
+ file_path = os.path.join(parent, filename)
+ if remote_parent == '.':
+ remote_filename = filename
+ else:
+ remote_filename = posixpath.join(remote_parent, filename)
+ file_size = os.path.getsize(file_path)
+ board_file_size = files_dict.get(remote_filename, -1)
+ # sys.stdout.write('name {0} size {1} {2}\n'.format(remote_filename, board_file_size, file_size))
+ # sys.stdout.flush()
+ if board_file_size != file_size:
+ with open(file_path, "rb") as infile:
+ file_name_list.append(remote_filename)
+ data_list.append(infile.read())
+ #board_files.put(remote_filename, infile.read())
+ else:
+ sys.stdout.write("Skip " + filename + "\n")
+ sys.stdout.flush()
+ board_files.putdir(file_name_list, data_list, False)
+
+ '''
+ for filename in child_files:
+ with open(os.path.join(parent, filename), "rb") as infile:
+ remote_filename = posixpath.join(remote_parent, filename)
+ board_files.put(remote_filename, infile.read())
+ '''
+
+ else:
+ if remote is None:
+ remote = os.path.basename(os.path.abspath(local))
+ # File copy, open the file and copy its contents to the board.
+ # Put the file on the board.
+ with open(local, "rb") as infile:
+ board_files = files.Files(_board)
+ board_files.put(remote, infile.read())
+
+
+@cli.command()
+@click.argument("remote_file")
+def rm(remote_file):
+ """Remove a file from the board.
+
+ Remove the specified file from the board's filesystem. Must specify one
+ argument which is the path to the file to delete. Note that this can't
+ delete directories which have files inside them, but can delete empty
+ directories.
+
+ For example to delete main.py from the root of a board run:
+
+ ampy --port /board/serial/port rm main.py
+ """
+ # Delete the provided file/directory on the board.
+ board_files = files.Files(_board)
+ board_files.rm(remote_file)
+
+
+@cli.command()
+@click.option(
+ "--missing-okay", is_flag=True, help="Ignore if the directory does not exist."
+)
+@click.argument("remote_folder")
+def rmdir(remote_folder, missing_okay):
+ """Forcefully remove a folder and all its children from the board.
+
+ Remove the specified folder from the board's filesystem. Must specify one
+ argument which is the path to the folder to delete. This will delete the
+ directory and ALL of its children recursively, use with caution!
+
+ For example to delete everything under /adafruit_library from the root of a
+ board run:
+
+ ampy --port /board/serial/port rmdir adafruit_library
+ """
+ # Delete the provided file/directory on the board.
+ board_files = files.Files(_board)
+ board_files.rmdir(remote_folder, missing_okay=missing_okay)
+
+@cli.command()
+@click.argument("oldname")
+@click.argument("newname")
+def rename(oldname, newname):
+ board_files = files.Files(_board)
+ board_files.rename(oldname, newname)
+
+
+@cli.command()
+@click.argument("local", required=True)
+@click.argument("remote", required=True)
+def cpdir(local, remote):
+ board_files = files.Files(_board)
+ board_files.cpdir(local, remote)
+
+
+@cli.command()
+@click.argument("local", required=True)
+@click.argument("remote", required=True)
+def cpfile(local, remote):
+ board_files = files.Files(_board)
+ board_files.cpfile(local, remote)
+
+
+@cli.command()
+@click.argument("local_file")
+@click.option(
+ "--no-output",
+ "-n",
+ is_flag=True,
+ help="Run the code without waiting for it to finish and print output. Use this when running code with main loops that never return.",
+)
+def run(local_file, no_output):
+ """Run a script and print its output.
+
+ Run will send the specified file to the board and execute it immediately.
+ Any output from the board will be printed to the console (note that this is
+ not a 'shell' and you can't send input to the program).
+
+ Note that if your code has a main or infinite loop you should add the --no-output
+ option. This will run the script and immediately exit without waiting for
+ the script to finish and print output.
+
+ For example to run a test.py script and print any output until it finishes:
+
+ ampy --port /board/serial/port run test.py
+
+ Or to run test.py and not wait for it to finish:
+
+ ampy --port /board/serial/port run --no-output test.py
+ """
+ # Run the provided file and print its output.
+ board_files = files.Files(_board)
+ try:
+ output = board_files.run(local_file, not no_output, not no_output)
+ if output is not None:
+ print(output.decode("utf-8"), end="")
+ except IOError:
+ click.echo(
+ "Failed to find or read input file: {0}".format(local_file), err=True
+ )
+
+
+@cli.command()
+@click.option(
+ "--bootloader", "mode", flag_value="BOOTLOADER", help="Reboot into the bootloader"
+)
+@click.option(
+ "--hard",
+ "mode",
+ flag_value="NORMAL",
+ help="Perform a hard reboot, including running init.py",
+)
+@click.option(
+ "--repl",
+ "mode",
+ flag_value="SOFT",
+ default=True,
+ help="Perform a soft reboot, entering the REPL [default]",
+)
+@click.option(
+ "--safe",
+ "mode",
+ flag_value="SAFE_MODE",
+ help="Perform a safe-mode reboot. User code will not be run and the filesystem will be writeable over USB",
+)
+def reset(mode):
+ """Perform soft reset/reboot of the board.
+
+ Will connect to the board and perform a reset. Depending on the board
+ and firmware, several different types of reset may be supported.
+
+ ampy --port /board/serial/port reset
+ """
+ _board.enter_raw_repl()
+ if mode == "SOFT":
+ _board.exit_raw_repl()
+ return
+
+ _board.exec_(
+ """if 1:
+ def on_next_reset(x):
+ try:
+ import microcontroller
+ except:
+ if x == 'NORMAL': return ''
+ return 'Reset mode only supported on CircuitPython'
+ try:
+ microcontroller.on_next_reset(getattr(microcontroller.RunMode, x))
+ except ValueError as e:
+ return str(e)
+ return ''
+ def reset():
+ try:
+ import microcontroller
+ except:
+ import machine as microcontroller
+ microcontroller.reset()
+ """
+ )
+ r = _board.eval("on_next_reset({})".format(repr(mode)))
+ print("here we are", repr(r))
+ if r:
+ click.echo(r, err=True)
+ return
+
+ try:
+ _board.exec_raw_no_follow("reset()")
+ except serial.serialutil.SerialException as e:
+ # An error is expected to occur, as the board should disconnect from
+ # serial when restarted via microcontroller.reset()
+ pass
+
+
+if __name__ == "__main__":
+ error_exit = False
+ try:
+ cli()
+ except BaseException as e:
+ if getattr(e, 'code', True):
+ print('Error: {}'.format(e))
+ error_exit = True
+ finally:
+ # Try to ensure the board serial connection is always gracefully closed.
+ if _board is not None:
+ try:
+ _board.close()
+ except:
+ # Swallow errors when attempting to close as it's just a best effort
+ # and shouldn't cause a new error or problem if the connection can't
+ # be closed.
+ pass
+ if error_exit:
+ sys.exit(1)
diff --git a/mixly/tools/python/ampy/files.py b/mixly/tools/python/ampy/files.py
new file mode 100644
index 00000000..da130546
--- /dev/null
+++ b/mixly/tools/python/ampy/files.py
@@ -0,0 +1,594 @@
+# Adafruit MicroPython Tool - File Operations
+# Author: Tony DiCola
+# Copyright (c) 2016 Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+import ast
+import textwrap
+import sys
+
+from ampy.pyboard import PyboardError
+
+
+BUFFER_SIZE = 32 # Amount of data to read or write to the serial port at a time.
+# This is kept small because small chips and USB to serial
+# bridges usually have very small buffers.
+
+
+class DirectoryExistsError(Exception):
+ pass
+
+
+class Files(object):
+ """Class to interact with a MicroPython board files over a serial connection.
+ Provides functions for listing, uploading, and downloading files from the
+ board's filesystem.
+ """
+
+ def __init__(self, pyboard):
+ """Initialize the MicroPython board files class using the provided pyboard
+ instance. In most cases you should create a Pyboard instance (from
+ pyboard.py) which connects to a board over a serial connection and pass
+ it in, but you can pass in other objects for testing, etc.
+ """
+ self._pyboard = pyboard
+
+ def get(self, filename):
+ """Retrieve the contents of the specified file and return its contents
+ as a byte string.
+ """
+ # Open the file and read it a few bytes at a time and print out the
+ # raw bytes. Be careful not to overload the UART buffer so only write
+ # a few bytes at a time, and don't use print since it adds newlines and
+ # expects string data.
+ command = """
+ import sys
+ import ubinascii
+ with open('{0}', 'rb') as infile:
+ while True:
+ result = infile.read({1})
+ if result == b'':
+ break
+ len = sys.stdout.write(ubinascii.hexlify(result))
+ """.format(
+ filename, BUFFER_SIZE
+ )
+ self._pyboard.enter_raw_repl()
+ try:
+ out = self._pyboard.exec_(textwrap.dedent(command))
+ except PyboardError as ex:
+ # Check if this is an OSError #2, i.e. file doesn't exist and
+ # rethrow it as something more descriptive.
+ try:
+ if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1:
+ raise RuntimeError("No such file: {0}".format(filename))
+ else:
+ raise ex
+ except UnicodeDecodeError:
+ raise ex
+ self._pyboard.exit_raw_repl()
+ return out
+
+ def ls(self, directory="/", long_format=True, recursive=False, exit_repl=True):
+ """List the contents of the specified directory (or root if none is
+ specified). Returns a list of strings with the names of files in the
+ specified directory. If long_format is True then a list of 2-tuples
+ with the name and size (in bytes) of the item is returned. Note that
+ it appears the size of directories is not supported by MicroPython and
+ will always return 0 (i.e. no recursive size computation).
+ """
+
+ # Disabling for now, see https://github.com/adafruit/ampy/issues/55.
+ # # Make sure directory ends in a slash.
+ # if not directory.endswith("/"):
+ # directory += "/"
+
+ # Make sure directory starts with slash, for consistency.
+ if not directory.startswith("/"):
+ directory = "/" + directory
+
+ command = """\
+ try:
+ import os
+ except ImportError:
+ import uos as os\n"""
+
+ if recursive:
+ command += """\
+ def listdir(directory):
+ result = set()
+
+ def _listdir(dir_or_file):
+ try:
+ # if its a directory, then it should provide some children.
+ children = os.listdir(dir_or_file)
+ except OSError:
+ # probably a file. run stat() to confirm.
+ os.stat(dir_or_file)
+ result.add(dir_or_file)
+ else:
+ # probably a directory, add to result if empty.
+ if children:
+ # queue the children to be dealt with in next iteration.
+ for child in children:
+ # create the full path.
+ if dir_or_file == '/':
+ next = dir_or_file + child
+ else:
+ next = dir_or_file + '/' + child
+
+ _listdir(next)
+ else:
+ result.add(dir_or_file)
+
+ _listdir(directory)
+ return sorted(result)\n"""
+ else:
+ command += """\
+ def check_path(path):
+ try:
+ stat = os.stat(path)
+ # The first element of stat contains the file type and permission information
+ # The mode index of the tuple returned by os.stat() is 0
+ mode = stat[0]
+ # To determine whether it is a directory, check the directory bit in stat mode
+ if mode & 0o170000 == 0o040000:
+ if len(os.listdir(path)):
+ return 'dir'
+ else:
+ return 'empty dir'
+ # To determine whether it is a file, check the file position in stat mode
+ elif mode & 0o170000 == 0o100000:
+ return 'file'
+ else:
+ return 'special file'
+ except OSError:
+ return 'none'
+
+ def listdir(directory):
+ output = []
+ if directory == '/':
+ dirs = sorted([directory + f for f in os.listdir(directory)])
+ else:
+ dirs = sorted([directory + '/' + f for f in os.listdir(directory)])
+
+ for dir in dirs:
+ info = check_path(dir)
+ if info == 'none':
+ continue
+ output.append([dir, info])
+ return output\n"""
+
+ # Execute os.listdir() command on the board.
+ if long_format:
+ command += """
+ r = []
+ for f in listdir('{0}'):
+ size = os.stat(f)[6]
+ r.append('{{0}} - {{1}} bytes'.format(f, size))
+ print(r)
+ """.format(
+ directory
+ )
+ else:
+ command += """
+ print(listdir('{0}'))
+ """.format(
+ directory
+ )
+ self._pyboard.enter_raw_repl()
+ try:
+ out = self._pyboard.exec_(textwrap.dedent(command))
+ except PyboardError as ex:
+ # Check if this is an OSError #2, i.e. directory doesn't exist and
+ # rethrow it as something more descriptive.
+ if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1:
+ raise RuntimeError("No such directory: {0}".format(directory))
+ else:
+ raise ex
+ if exit_repl:
+ self._pyboard.exit_raw_repl()
+ # Parse the result list and return it.
+ return ast.literal_eval(out.decode("utf-8"))
+
+ def getFilesInfo(self, directory="/", recursive=False):
+ """List the contents of the specified directory (or root if none is
+ specified). Returns a list of strings with the names of files in the
+ specified directory. If long_format is True then a list of 2-tuples
+ with the name and size (in bytes) of the item is returned. Note that
+ it appears the size of directories is not supported by MicroPython and
+ will always return 0 (i.e. no recursive size computation).
+ """
+
+ # Disabling for now, see https://github.com/adafruit/ampy/issues/55.
+ # # Make sure directory ends in a slash.
+ # if not directory.endswith("/"):
+ # directory += "/"
+
+ # Make sure directory starts with slash, for consistency.
+ # if not directory.startswith("/"):
+ # directory = "/" + directory
+
+ command = """\
+ try:
+ import os
+ except ImportError:
+ import uos as os\n"""
+
+ if recursive:
+ command += """\
+ def listdir(directory):
+ result = set()
+
+ def _listdir(dir_or_file):
+ try:
+ # if its a directory, then it should provide some children.
+ children = os.listdir(dir_or_file)
+ except OSError:
+ # probably a file. run stat() to confirm.
+ os.stat(dir_or_file)
+ result.add(dir_or_file)
+ else:
+ # probably a directory, add to result if empty.
+ if children:
+ # queue the children to be dealt with in next iteration.
+ for child in children:
+ # create the full path.
+ if dir_or_file == '/':
+ next = dir_or_file + child
+ else:
+ next = dir_or_file + '/' + child
+
+ _listdir(next)
+ else:
+ result.add(dir_or_file)
+
+ _listdir(directory)
+ return sorted(result)\n"""
+ else:
+ command += """\
+ def listdir(directory):
+ try:
+ if directory == '/':
+ return sorted([directory + f for f in os.listdir(directory)])
+ else:
+ return sorted([directory + '/' + f for f in os.listdir(directory)])
+ except:
+ return sorted([f for f in os.listdir()])\n"""
+
+ # Execute os.listdir() command on the board.
+ # 当command执行出错时执行command1
+ command2 = command
+ command2 += """
+ r = []
+ for f in listdir('{0}'):
+ try:
+ size = os.stat(f)[6]
+ except:
+ size = os.size(f)
+ r.append([f, size])
+ print(r)
+ """.format(
+ (directory if directory else "/")
+ )
+ command += """
+ r = []
+ for f in listdir('{0}'):
+ try:
+ size = os.stat(f)[6]
+ except:
+ size = os.size(f)
+ r.append([f, size])
+ print(r)
+ """.format(
+ directory
+ )
+ try:
+ out = self._pyboard.exec_(textwrap.dedent(command))
+ except PyboardError as ex:
+ out = self._pyboard.exec_(textwrap.dedent(command2))
+ # Check if this is an OSError #2, i.e. directory doesn't exist and
+ # rethrow it as something more descriptive.
+ except PyboardError as ex:
+ if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1:
+ raise RuntimeError("No such directory: {0}".format(directory))
+ else:
+ raise ex
+ # Parse the result list and return it.
+ try:
+ return ast.literal_eval(out.decode("utf-8"))
+ except:
+ return ''
+
+ def mkdir(self, directory, exists_okay=False):
+ """Create the specified directory. Note this cannot create a recursive
+ hierarchy of directories, instead each one should be created separately.
+ """
+ # Execute os.mkdir command on the board.
+ command = """
+ try:
+ import os
+ except ImportError:
+ import uos as os
+ os.mkdir('{0}')
+ """.format(
+ directory
+ )
+ self._pyboard.enter_raw_repl()
+ try:
+ out = self._pyboard.exec_(textwrap.dedent(command))
+ except PyboardError as ex:
+ # Check if this is an OSError #17, i.e. directory already exists.
+ if ex.args[2].decode("utf-8").find("OSError: [Errno 17] EEXIST") != -1:
+ if not exists_okay:
+ raise DirectoryExistsError(
+ "Directory already exists: {0}".format(directory)
+ )
+ else:
+ raise ex
+ self._pyboard.exit_raw_repl()
+
+ def mkfile(self, file, exists_okay=False):
+ command = """
+ try:
+ import os
+ except ImportError:
+ import uos as os
+
+ try:
+ os.stat('{0}')
+ except OSError:
+ f = open('{0}', 'w')
+ f.close()
+ """.format(
+ file
+ )
+ self._pyboard.enter_raw_repl()
+ try:
+ out = self._pyboard.exec_(textwrap.dedent(command))
+ except PyboardError as ex:
+ raise ex
+ self._pyboard.exit_raw_repl()
+
+ def put(self, filename, data, enter_repl=True, exit_repl=True):
+ """Create or update the specified file with the provided data.
+ """
+ # Open the file for writing on the board and write chunks of data.
+ if enter_repl:
+ self._pyboard.enter_raw_repl()
+ self._pyboard.exec_("f = open('{0}', 'wb')".format(filename))
+ sys.stdout.write("Write " + filename)
+ size = len(data)
+ # Loop through and write a buffer size chunk of data at a time.
+ for i in range(0, size, BUFFER_SIZE):
+ chunk_size = min(BUFFER_SIZE, size - i)
+ chunk = repr(data[i : i + chunk_size])
+ # Make sure to send explicit byte strings (handles python 2 compatibility).
+ if not chunk.startswith("b"):
+ chunk = "b" + chunk
+ self._pyboard.exec_("f.write({0})".format(chunk))
+ self._pyboard.exec_("f.close()")
+ sys.stdout.write(" Done!\n")
+ if exit_repl:
+ self._pyboard.exit_raw_repl()
+
+ def putdir(self, fileNameList, dataList, enter_repl=True, exit_repl=True):
+ """Create or update the specified file with the provided data.
+ """
+ # Open the file for writing on the board and write chunks of data.
+ if enter_repl:
+ self._pyboard.enter_raw_repl()
+ for i in range(0, len(fileNameList), 1):
+ self._pyboard.exec_("f = open('{0}', 'wb')".format(fileNameList[i]))
+ sys.stdout.write("Writing " + fileNameList[i])
+ sys.stdout.flush()
+ #print("write " + fileNameList[i])
+ data = dataList[i]
+ size = len(data)
+ # Loop through and write a buffer size chunk of data at a time.
+ for i in range(0, size, BUFFER_SIZE):
+ chunk_size = min(BUFFER_SIZE, size - i)
+ chunk = repr(data[i : i + chunk_size])
+ # Make sure to send explicit byte strings (handles python 2 compatibility).
+ if not chunk.startswith("b"):
+ chunk = "b" + chunk
+ self._pyboard.exec_("f.write({0})".format(chunk))
+ self._pyboard.exec_("f.close()")
+ sys.stdout.write(" Done!\n")
+ sys.stdout.flush()
+ if exit_repl:
+ self._pyboard.exit_raw_repl()
+
+ def rm(self, filename):
+ """Remove the specified file or directory."""
+ command = """
+ try:
+ import os
+ except ImportError:
+ import uos as os
+ os.remove('{0}')
+ """.format(
+ filename
+ )
+ self._pyboard.enter_raw_repl()
+ try:
+ out = self._pyboard.exec_(textwrap.dedent(command))
+ except PyboardError as ex:
+ message = ex.args[2].decode("utf-8")
+ # Check if this is an OSError #2, i.e. file/directory doesn't exist
+ # and rethrow it as something more descriptive.
+ if message.find("OSError: [Errno 2] ENOENT") != -1:
+ raise RuntimeError("No such file/directory: {0}".format(filename))
+ # Check for OSError #13, the directory isn't empty.
+ if message.find("OSError: [Errno 13] EACCES") != -1:
+ raise RuntimeError("Directory is not empty: {0}".format(filename))
+ else:
+ raise ex
+ self._pyboard.exit_raw_repl()
+
+ def rmdir(self, directory, missing_okay=False):
+ """Forcefully remove the specified directory and all its children."""
+ # Build a script to walk an entire directory structure and delete every
+ # file and subfolder. This is tricky because MicroPython has no os.walk
+ # or similar function to walk folders, so this code does it manually
+ # with recursion and changing directories. For each directory it lists
+ # the files and deletes everything it can, i.e. all the files. Then
+ # it lists the files again and assumes they are directories (since they
+ # couldn't be deleted in the first pass) and recursively clears those
+ # subdirectories. Finally when finished clearing all the children the
+ # parent directory is deleted.
+ command = """
+ try:
+ import os
+ except ImportError:
+ import uos as os
+
+ def rmdir(directory):
+ os.chdir(directory)
+ for f in os.listdir():
+ try:
+ os.remove(f)
+ except OSError:
+ pass
+ for f in os.listdir():
+ rmdir(f)
+ os.chdir('..')
+ os.rmdir(directory)
+ rmdir('{0}')
+ """.format(
+ directory
+ )
+ self._pyboard.enter_raw_repl()
+ try:
+ out = self._pyboard.exec_(textwrap.dedent(command))
+ except PyboardError as ex:
+ message = ex.args[2].decode("utf-8")
+ # Check if this is an OSError #2, i.e. directory doesn't exist
+ # and rethrow it as something more descriptive.
+ if message.find("OSError: [Errno 2] ENOENT") != -1:
+ if not missing_okay:
+ raise RuntimeError("No such directory: {0}".format(directory))
+ else:
+ raise ex
+ self._pyboard.exit_raw_repl()
+
+ def rename(self, oldname, newname):
+ command = """
+ try:
+ import os
+ except ImportError:
+ import uos as os
+
+ os.rename('{0}', '{1}')
+ """.format(
+ oldname, newname
+ )
+ self._pyboard.enter_raw_repl()
+ try:
+ out = self._pyboard.exec_(textwrap.dedent(command))
+ except PyboardError as ex:
+ message = ex.args[2].decode("utf-8")
+ raise ex
+ self._pyboard.exit_raw_repl()
+
+ def cpdir(self, oldpath, newpath):
+ command = """
+ try:
+ import os
+ except ImportError:
+ import uos as os
+
+ def cpfile(src, dst):
+ with open(src, 'rb') as src_file:
+ content = src_file.read()
+ with open(dst, 'wb') as dst_file:
+ dst_file.write(content)
+
+ def cpdir(src, dst):
+ try:
+ os.mkdir(dst)
+ except:
+ pass
+ for item in os.listdir(src):
+ src_path = src + '/' + item
+ dst_path = dst + '/' + item
+ stat = os.stat(src_path)
+ mode = stat[0]
+ if mode & 0o170000 == 0o040000:
+ cpdir(src_path, dst_path)
+ else:
+ cpfile(src_path, dst_path)
+
+ cpdir('{0}', '{1}')
+ """.format(
+ oldpath, newpath
+ )
+ self._pyboard.enter_raw_repl()
+ try:
+ out = self._pyboard.exec_(textwrap.dedent(command))
+ except PyboardError as ex:
+ message = ex.args[2].decode("utf-8")
+ raise ex
+ self._pyboard.exit_raw_repl()
+
+ def cpfile(self, oldpath, newpath):
+ command = """
+ try:
+ import os
+ except ImportError:
+ import uos as os
+ def cpfile(src, dst):
+ with open(src, 'rb') as src_file:
+ content = src_file.read()
+ with open(dst, 'wb') as dst_file:
+ dst_file.write(content)
+
+ cpfile('{0}', '{1}')
+ """.format(
+ oldpath, newpath
+ )
+ self._pyboard.enter_raw_repl()
+ try:
+ out = self._pyboard.exec_(textwrap.dedent(command))
+ except PyboardError as ex:
+ message = ex.args[2].decode("utf-8")
+ raise ex
+ self._pyboard.exit_raw_repl()
+
+ def run(self, filename, wait_output=True, stream_output=True):
+ """Run the provided script and return its output. If wait_output is True
+ (default) then wait for the script to finish and then return its output,
+ otherwise just run the script and don't wait for any output.
+ If stream_output is True(default) then return None and print outputs to
+ stdout without buffering.
+ """
+ self._pyboard.enter_raw_repl()
+ out = None
+ if stream_output:
+ self._pyboard.execfile(filename, stream_output=True)
+ elif wait_output:
+ # Run the file and wait for output to return.
+ out = self._pyboard.execfile(filename)
+ else:
+ # Read the file and run it using lower level pyboard functions that
+ # won't wait for it to finish or return output.
+ with open(filename, "rb") as infile:
+ self._pyboard.exec_raw_no_follow(infile.read())
+ self._pyboard.exit_raw_repl()
+ return out
diff --git a/mixly/tools/python/ampy/pyboard.py b/mixly/tools/python/ampy/pyboard.py
new file mode 100644
index 00000000..a8bf3497
--- /dev/null
+++ b/mixly/tools/python/ampy/pyboard.py
@@ -0,0 +1,448 @@
+#!/usr/bin/env python
+
+"""
+pyboard interface
+
+This module provides the Pyboard class, used to communicate with and
+control the pyboard over a serial USB connection.
+
+Example usage:
+
+ import pyboard
+ pyb = pyboard.Pyboard('/dev/ttyACM0')
+
+Or:
+
+ pyb = pyboard.Pyboard('192.168.1.1')
+
+Then:
+
+ pyb.enter_raw_repl()
+ pyb.exec('pyb.LED(1).on()')
+ pyb.exit_raw_repl()
+
+Note: if using Python2 then pyb.exec must be written as pyb.exec_.
+To run a script from the local machine on the board and print out the results:
+
+ import pyboard
+ pyboard.execfile('test.py', device='/dev/ttyACM0')
+
+This script can also be run directly. To execute a local script, use:
+
+ ./pyboard.py test.py
+
+Or:
+
+ python pyboard.py test.py
+
+"""
+
+import sys
+import time
+import json
+
+_rawdelay = None
+
+try:
+ stdout = sys.stdout.buffer
+except AttributeError:
+ # Python2 doesn't have buffer attr
+ stdout = sys.stdout
+
+def stdout_write_bytes(b):
+ b = b.replace(b"\x04", b"")
+ stdout.write(b)
+ stdout.flush()
+
+class PyboardError(BaseException):
+ pass
+
+class TelnetToSerial:
+ def __init__(self, ip, user, password, read_timeout=None):
+ import telnetlib
+ self.tn = telnetlib.Telnet(ip, timeout=15)
+ self.read_timeout = read_timeout
+ if b'Login as:' in self.tn.read_until(b'Login as:', timeout=read_timeout):
+ self.tn.write(bytes(user, 'ascii') + b"\r\n")
+
+ if b'Password:' in self.tn.read_until(b'Password:', timeout=read_timeout):
+ # needed because of internal implementation details of the telnet server
+ time.sleep(0.2)
+ self.tn.write(bytes(password, 'ascii') + b"\r\n")
+
+ if b'for more information.' in self.tn.read_until(b'Type "help()" for more information.', timeout=read_timeout):
+ # login succesful
+ from collections import deque
+ self.fifo = deque()
+ return
+
+ raise PyboardError('Failed to establish a telnet connection with the board')
+
+ def __del__(self):
+ self.close()
+
+ def close(self):
+ try:
+ self.tn.close()
+ except:
+ # the telnet object might not exist yet, so ignore this one
+ pass
+
+ def read(self, size=1):
+ while len(self.fifo) < size:
+ timeout_count = 0
+ data = self.tn.read_eager()
+ if len(data):
+ self.fifo.extend(data)
+ timeout_count = 0
+ else:
+ time.sleep(0.25)
+ if self.read_timeout is not None and timeout_count > 4 * self.read_timeout:
+ break
+ timeout_count += 1
+
+ data = b''
+ while len(data) < size and len(self.fifo) > 0:
+ data += bytes([self.fifo.popleft()])
+ return data
+
+ def write(self, data):
+ self.tn.write(data)
+ return len(data)
+
+ def inWaiting(self):
+ n_waiting = len(self.fifo)
+ if not n_waiting:
+ data = self.tn.read_eager()
+ self.fifo.extend(data)
+ return len(data)
+ else:
+ return n_waiting
+
+class Pyboard:
+ def __init__(self, device, baudrate=115200, user='micro', password='python', wait=0, rawdelay=0, boardreset="{}", file_empty="main.py", info=True):
+ self.boardreset = json.loads(boardreset);
+ self.file_empty = file_empty;
+ self.info = info;
+ global _rawdelay
+ _rawdelay = rawdelay
+ if device and device[0].isdigit() and device[-1].isdigit() and device.count('.') == 3:
+ # device looks like an IP address
+ self.serial = TelnetToSerial(device, user, password, read_timeout=10)
+ else:
+ import serial
+ delayed = False
+ for attempt in range(wait + 1):
+ try:
+ self.serial = serial.Serial(device, baudrate=baudrate, interCharTimeout=10, writeTimeout=5, timeout=40)
+ break
+ except (OSError, IOError): # Py2 and Py3 have different errors
+ if wait == 0:
+ continue
+ if attempt == 0:
+ sys.stdout.write('Waiting {} seconds for pyboard '.format(wait))
+ delayed = True
+ time.sleep(1)
+ #sys.stdout.write('.')
+ #sys.stdout.flush()
+ else:
+ if delayed:
+ print('')
+ raise PyboardError('failed to access ' + device)
+ if delayed:
+ print('')
+
+ def close(self):
+ self.serial.close()
+
+ def read_until(self, min_num_bytes, ending, timeout=1, data_consumer=None):
+ data = self.serial.read(min_num_bytes)
+ if data_consumer:
+ data_consumer(data)
+ timeout_count = 0
+ while True:
+ if data.endswith(ending) or data.lower().endswith(ending):
+ break
+ elif self.serial.inWaiting() > 0:
+ new_data = self.serial.read(1)
+ data = data + new_data
+ if data_consumer:
+ data_consumer(new_data)
+ timeout_count = 0
+ else:
+ timeout_count += 1
+ if timeout is not None and timeout_count >= 10 * timeout:
+ break
+ time.sleep(0.01)
+ return data
+
+ def enter_raw_repl(self):
+ # Brief delay before sending RAW MODE char if requests
+ if _rawdelay > 0:
+ time.sleep(_rawdelay)
+ # ctrl-C twice: interrupt any running program
+ #sys.stdout.write("Try to delete ./{} ".format(self.file_empty))
+ if self.info:
+ sys.stdout.write("Try to enter REPL ")
+ sys.stdout.flush()
+ repl_ok=False
+ oldInterCharTimeout = self.serial.interCharTimeout
+ oldTimeout = self.serial.timeout
+ self.serial.interCharTimeout = 1
+ self.serial.timeout = 1
+ try:
+ for retry in range(10):
+ self.serial.write(b'\r\x02\x03')
+ time.sleep(0.1)
+ self.serial.write(b'\x02\x03')
+ time.sleep(0.1)
+ data = self.read_until(1, b'>')
+ if self.info:
+ sys.stdout.write(".")
+ sys.stdout.flush()
+ if data.endswith(b'>'):
+ repl_ok=True
+ if self.info:
+ sys.stdout.write("\n")
+ sys.stdout.write("Delete {} ".format(self.file_empty))
+ sys.stdout.flush()
+ self.serial.write(bytes("import os; os.remove('{}')\r\n".format(self.file_empty), encoding="utf8"))
+ time.sleep(0.1)
+ if self.info:
+ sys.stdout.write("Done!\n")
+ sys.stdout.flush()
+ break
+ if retry >8:
+ if self.info:
+ sys.stdout.write("\n")
+ sys.stdout.write('could not enter raw repl, Try to reset\n')
+ sys.stdout.flush()
+ break
+ finally:
+ self.serial.interCharTimeout = oldInterCharTimeout
+ self.serial.timeout = oldTimeout
+ if not repl_ok:
+ if self.info:
+ sys.stdout.write("Reset Start\n")
+ sys.stdout.flush()
+ resetLen = len(self.boardreset)
+ if resetLen:
+ for i in range(0, resetLen, 1):
+ dtr = self.boardreset[i].get("dtr", -1)
+ rts = self.boardreset[i].get("rts", -1)
+ sleep = self.boardreset[i].get("sleep", -1)
+ if dtr != -1 and rts != -1:
+ self.serial.setDTR (dtr)
+ self.serial.setRTS (rts)
+ if self.info:
+ sys.stdout.write("Set dtr:{}, rts:{}\n".format(dtr, rts))
+ elif sleep != -1:
+ time.sleep(sleep / 1000)
+ if self.info:
+ sys.stdout.write("Set sleep:{}s\n".format(sleep / 1000))
+ if self.info:
+ sys.stdout.flush()
+ else:
+ self.serial.setDTR (False)
+ self.serial.setRTS (False)
+ if self.info:
+ sys.stdout.write("Set dtr:{}, rts:{}\n".format(False, False))
+ sys.stdout.flush()
+ time.sleep(0.1)
+ if self.info:
+ sys.stdout.write("Set sleep:{}s\n".format(0.1))
+ sys.stdout.flush()
+ self.serial.setDTR (True)
+ self.serial.setRTS (True)
+ if self.info:
+ sys.stdout.write("Set dtr:{}, rts:{}\n".format(True, True))
+ sys.stdout.flush()
+ time.sleep(0.1)
+ if self.info:
+ sys.stdout.write("Set sleep:{}s\n".format(0.1))
+ sys.stdout.flush()
+ if self.info:
+ sys.stdout.write("Reset Done!\n")
+ sys.stdout.flush()
+
+ # Brief delay before sending RAW MODE char if requests
+ if _rawdelay > 0:
+ time.sleep(_rawdelay)
+ # ctrl-C twice: interrupt any running program
+ self.serial.write(b'\r\x03')
+ time.sleep(0.1)
+ self.serial.write(b'\x03')
+ time.sleep(0.1)
+
+ # flush input (without relying on serial.flushInput())
+ n = self.serial.inWaiting()
+ while n > 0:
+ self.serial.read(n)
+ n = self.serial.inWaiting()
+ #time.sleep(2)
+ #self.serial.write(b'\x03\x04')
+ for retry in range(0, 5):
+ self.serial.write(b'\r\x01') # ctrl-A: enter raw REPL
+ data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n>')
+ if data.endswith(b'raw REPL; CTRL-B to exit\r\n>'):
+ break
+ else:
+ if retry >= 4:
+ raise PyboardError('could not enter raw repl')
+ else:
+ self.serial.write(b'\r\x03')
+ time.sleep(0.1)
+ self.serial.write(b'\x03')
+ time.sleep(0.1)
+ self.serial.write(b'\x04') # ctrl-D: soft reset
+ data = self.read_until(1, b'soft reboot\r\n')
+ if not data.lower().endswith(b'soft reboot\r\n'):
+ self.serial.write(b'\x04') # ctrl-D: soft reset
+ raise PyboardError('could not enter raw repl')
+ # By splitting this into 2 reads, it allows boot.py to print stuff,
+ # which will show up after the soft reboot and before the raw REPL.
+ # Modification from original pyboard.py below:
+ # Add a small delay and send Ctrl-C twice after soft reboot to ensure
+ # any main program loop in main.py is interrupted.
+ time.sleep(0.5)
+ self.serial.write(b'\x03')
+ time.sleep(0.1) # (slight delay before second interrupt
+ self.serial.write(b'\x03')
+ # End modification above.
+ data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n')
+ if not data.endswith(b'raw REPL; CTRL-B to exit\r\n'):
+ raise PyboardError('could not enter raw repl')
+
+ def exit_raw_repl(self):
+ self.serial.write(b'\r\x02') # ctrl-B: enter friendly REPL
+
+ def follow(self, timeout, data_consumer=None):
+ # wait for normal output
+ data = self.read_until(1, b'\x04', timeout=timeout, data_consumer=data_consumer)
+ if not data.endswith(b'\x04'):
+ raise PyboardError('timeout waiting for first EOF reception')
+ data = data[:-1]
+
+ # wait for error output
+ data_err = self.read_until(1, b'\x04', timeout=timeout)
+ if not data_err.endswith(b'\x04'):
+ raise PyboardError('timeout waiting for second EOF reception')
+ data_err = data_err[:-1]
+
+ # return normal and error output
+ return data, data_err
+
+ def exec_raw_no_follow(self, command):
+ if isinstance(command, bytes):
+ command_bytes = command
+ else:
+ command_bytes = bytes(command, encoding='utf8')
+
+ # check we have a prompt
+ data = self.read_until(1, b'>')
+ if not data.endswith(b'>'):
+ raise PyboardError('could not enter raw repl')
+
+ # write command
+ for i in range(0, len(command_bytes), 256):
+ self.serial.write(command_bytes[i:min(i + 256, len(command_bytes))])
+ time.sleep(0.01)
+ self.serial.write(b'\x04')
+
+ # check if we could exec command
+ data = self.serial.read(2)
+ if data != b'OK' and data != b'ra':
+ raise PyboardError('could not exec command')
+
+ def exec_raw(self, command, timeout=10, data_consumer=None):
+ self.exec_raw_no_follow(command);
+ return self.follow(timeout, data_consumer)
+
+ def eval(self, expression):
+ ret = self.exec_('print({})'.format(expression))
+ ret = ret.strip()
+ return ret
+
+ def exec_(self, command, stream_output=False):
+ data_consumer = None
+ if stream_output:
+ data_consumer = stdout_write_bytes
+ ret, ret_err = self.exec_raw(command, data_consumer=data_consumer)
+ if ret_err:
+ raise PyboardError('exception', ret, ret_err)
+ return ret
+
+ def execfile(self, filename, stream_output=False):
+ with open(filename, 'rb') as f:
+ pyfile = f.read()
+ return self.exec_(pyfile, stream_output=stream_output)
+
+ def get_time(self):
+ t = str(self.eval('pyb.RTC().datetime()'), encoding='utf8')[1:-1].split(', ')
+ return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6])
+
+# in Python2 exec is a keyword so one must use "exec_"
+# but for Python3 we want to provide the nicer version "exec"
+setattr(Pyboard, "exec", Pyboard.exec_)
+
+def execfile(filename, device='/dev/ttyACM0', baudrate=115200, user='micro', password='python'):
+ pyb = Pyboard(device, baudrate, user, password)
+ pyb.enter_raw_repl()
+ output = pyb.execfile(filename)
+ stdout_write_bytes(output)
+ pyb.exit_raw_repl()
+ pyb.close()
+
+def main():
+ import argparse
+ cmd_parser = argparse.ArgumentParser(description='Run scripts on the pyboard.')
+ cmd_parser.add_argument('--device', default='/dev/ttyACM0', help='the serial device or the IP address of the pyboard')
+ cmd_parser.add_argument('-b', '--baudrate', default=115200, help='the baud rate of the serial device')
+ cmd_parser.add_argument('-u', '--user', default='micro', help='the telnet login username')
+ cmd_parser.add_argument('-p', '--password', default='python', help='the telnet login password')
+ cmd_parser.add_argument('-c', '--command', help='program passed in as string')
+ cmd_parser.add_argument('-w', '--wait', default=0, type=int, help='seconds to wait for USB connected board to become available')
+ cmd_parser.add_argument('--follow', action='store_true', help='follow the output after running the scripts [default if no scripts given]')
+ cmd_parser.add_argument('files', nargs='*', help='input files')
+ args = cmd_parser.parse_args()
+
+ def execbuffer(buf):
+ try:
+ pyb = Pyboard(args.device, args.baudrate, args.user, args.password, args.wait)
+ pyb.enter_raw_repl()
+ ret, ret_err = pyb.exec_raw(buf, timeout=None, data_consumer=stdout_write_bytes)
+ pyb.exit_raw_repl()
+ pyb.close()
+ except PyboardError as er:
+ print(er)
+ sys.exit(1)
+ except KeyboardInterrupt:
+ sys.exit(1)
+ if ret_err:
+ stdout_write_bytes(ret_err)
+ sys.exit(1)
+
+ if args.command is not None:
+ execbuffer(args.command.encode('utf-8'))
+
+ for filename in args.files:
+ with open(filename, 'rb') as f:
+ pyfile = f.read()
+ execbuffer(pyfile)
+
+ if args.follow or (args.command is None and len(args.files) == 0):
+ try:
+ pyb = Pyboard(args.device, args.baudrate, args.user, args.password, args.wait)
+ ret, ret_err = pyb.follow(timeout=None, data_consumer=stdout_write_bytes)
+ pyb.close()
+ except PyboardError as er:
+ print(er)
+ sys.exit(1)
+ except KeyboardInterrupt:
+ sys.exit(1)
+ if ret_err:
+ stdout_write_bytes(ret_err)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
diff --git a/mixly/tools/python/ampy_main.py b/mixly/tools/python/ampy_main.py
new file mode 100644
index 00000000..899963f5
--- /dev/null
+++ b/mixly/tools/python/ampy_main.py
@@ -0,0 +1,24 @@
+import sys
+from ampy.cli import cli, _board
+
+
+if __name__ == "__main__":
+ error_exit = False
+ try:
+ cli()
+ except BaseException as e:
+ if getattr(e, 'code', True):
+ print('Error: {}'.format(e))
+ error_exit = True
+ finally:
+ # Try to ensure the board serial connection is always gracefully closed.
+ if _board is not None:
+ try:
+ _board.close()
+ except:
+ # Swallow errors when attempting to close as it's just a best effort
+ # and shouldn't cause a new error or problem if the connection can't
+ # be closed.
+ pass
+ if error_exit:
+ sys.exit(1)
\ No newline at end of file
diff --git a/mixly/tools/python/backports/__init__.py b/mixly/tools/python/backports/__init__.py
new file mode 100644
index 00000000..36a5b559
--- /dev/null
+++ b/mixly/tools/python/backports/__init__.py
@@ -0,0 +1,4 @@
+# See https://pypi.python.org/pypi/backports
+
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
diff --git a/mixly/tools/python/backports/tempfile.py b/mixly/tools/python/backports/tempfile.py
new file mode 100644
index 00000000..de3d79de
--- /dev/null
+++ b/mixly/tools/python/backports/tempfile.py
@@ -0,0 +1,75 @@
+"""
+Partial backport of Python 3.5's tempfile module:
+
+ TemporaryDirectory
+
+Backport modifications are marked with marked with "XXX backport".
+"""
+from __future__ import absolute_import
+
+import sys
+import warnings as _warnings
+from shutil import rmtree as _rmtree
+
+from backports.weakref import finalize
+
+
+# XXX backport: Rather than backporting all of mkdtemp(), we just create a
+# thin wrapper implementing its Python 3.5 signature.
+if sys.version_info < (3, 5):
+ from tempfile import mkdtemp as old_mkdtemp
+
+ def mkdtemp(suffix=None, prefix=None, dir=None):
+ """
+ Wrap `tempfile.mkdtemp()` to make the suffix and prefix optional (like Python 3.5).
+ """
+ kwargs = {k: v for (k, v) in
+ dict(suffix=suffix, prefix=prefix, dir=dir).items()
+ if v is not None}
+ return old_mkdtemp(**kwargs)
+
+else:
+ from tempfile import mkdtemp
+
+
+# XXX backport: ResourceWarning was added in Python 3.2.
+# For earlier versions, fall back to RuntimeWarning instead.
+_ResourceWarning = RuntimeWarning if sys.version_info < (3, 2) else ResourceWarning
+
+
+class TemporaryDirectory(object):
+ """Create and return a temporary directory. This has the same
+ behavior as mkdtemp but can be used as a context manager. For
+ example:
+
+ with TemporaryDirectory() as tmpdir:
+ ...
+
+ Upon exiting the context, the directory and everything contained
+ in it are removed.
+ """
+
+ def __init__(self, suffix=None, prefix=None, dir=None):
+ self.name = mkdtemp(suffix, prefix, dir)
+ self._finalizer = finalize(
+ self, self._cleanup, self.name,
+ warn_message="Implicitly cleaning up {!r}".format(self))
+
+ @classmethod
+ def _cleanup(cls, name, warn_message):
+ _rmtree(name)
+ _warnings.warn(warn_message, _ResourceWarning)
+
+
+ def __repr__(self):
+ return "<{} {!r}>".format(self.__class__.__name__, self.name)
+
+ def __enter__(self):
+ return self.name
+
+ def __exit__(self, exc, value, tb):
+ self.cleanup()
+
+ def cleanup(self):
+ if self._finalizer.detach():
+ _rmtree(self.name)
diff --git a/mixly/tools/python/backports/weakref.py b/mixly/tools/python/backports/weakref.py
new file mode 100644
index 00000000..de6193bd
--- /dev/null
+++ b/mixly/tools/python/backports/weakref.py
@@ -0,0 +1,151 @@
+"""
+Partial backport of Python 3.6's weakref module:
+
+ finalize (new in Python 3.4)
+
+Backport modifications are marked with "XXX backport".
+"""
+from __future__ import absolute_import
+
+import itertools
+import sys
+from weakref import ref
+
+__all__ = ['finalize']
+
+
+class finalize(object):
+ """Class for finalization of weakrefable objects
+
+ finalize(obj, func, *args, **kwargs) returns a callable finalizer
+ object which will be called when obj is garbage collected. The
+ first time the finalizer is called it evaluates func(*arg, **kwargs)
+ and returns the result. After this the finalizer is dead, and
+ calling it just returns None.
+
+ When the program exits any remaining finalizers for which the
+ atexit attribute is true will be run in reverse order of creation.
+ By default atexit is true.
+ """
+
+ # Finalizer objects don't have any state of their own. They are
+ # just used as keys to lookup _Info objects in the registry. This
+ # ensures that they cannot be part of a ref-cycle.
+
+ __slots__ = ()
+ _registry = {}
+ _shutdown = False
+ _index_iter = itertools.count()
+ _dirty = False
+ _registered_with_atexit = False
+
+ class _Info(object):
+ __slots__ = ("weakref", "func", "args", "kwargs", "atexit", "index")
+
+ def __init__(self, obj, func, *args, **kwargs):
+ if not self._registered_with_atexit:
+ # We may register the exit function more than once because
+ # of a thread race, but that is harmless
+ import atexit
+ atexit.register(self._exitfunc)
+ finalize._registered_with_atexit = True
+ info = self._Info()
+ info.weakref = ref(obj, self)
+ info.func = func
+ info.args = args
+ info.kwargs = kwargs or None
+ info.atexit = True
+ info.index = next(self._index_iter)
+ self._registry[self] = info
+ finalize._dirty = True
+
+ def __call__(self, _=None):
+ """If alive then mark as dead and return func(*args, **kwargs);
+ otherwise return None"""
+ info = self._registry.pop(self, None)
+ if info and not self._shutdown:
+ return info.func(*info.args, **(info.kwargs or {}))
+
+ def detach(self):
+ """If alive then mark as dead and return (obj, func, args, kwargs);
+ otherwise return None"""
+ info = self._registry.get(self)
+ obj = info and info.weakref()
+ if obj is not None and self._registry.pop(self, None):
+ return (obj, info.func, info.args, info.kwargs or {})
+
+ def peek(self):
+ """If alive then return (obj, func, args, kwargs);
+ otherwise return None"""
+ info = self._registry.get(self)
+ obj = info and info.weakref()
+ if obj is not None:
+ return (obj, info.func, info.args, info.kwargs or {})
+
+ @property
+ def alive(self):
+ """Whether finalizer is alive"""
+ return self in self._registry
+
+ @property
+ def atexit(self):
+ """Whether finalizer should be called at exit"""
+ info = self._registry.get(self)
+ return bool(info) and info.atexit
+
+ @atexit.setter
+ def atexit(self, value):
+ info = self._registry.get(self)
+ if info:
+ info.atexit = bool(value)
+
+ def __repr__(self):
+ info = self._registry.get(self)
+ obj = info and info.weakref()
+ if obj is None:
+ return '<%s object at %#x; dead>' % (type(self).__name__, id(self))
+ else:
+ return '<%s object at %#x; for %r at %#x>' % \
+ (type(self).__name__, id(self), type(obj).__name__, id(obj))
+
+ @classmethod
+ def _select_for_exit(cls):
+ # Return live finalizers marked for exit, oldest first
+ L = [(f,i) for (f,i) in cls._registry.items() if i.atexit]
+ L.sort(key=lambda item:item[1].index)
+ return [f for (f,i) in L]
+
+ @classmethod
+ def _exitfunc(cls):
+ # At shutdown invoke finalizers for which atexit is true.
+ # This is called once all other non-daemonic threads have been
+ # joined.
+ reenable_gc = False
+ try:
+ if cls._registry:
+ import gc
+ if gc.isenabled():
+ reenable_gc = True
+ gc.disable()
+ pending = None
+ while True:
+ if pending is None or finalize._dirty:
+ pending = cls._select_for_exit()
+ finalize._dirty = False
+ if not pending:
+ break
+ f = pending.pop()
+ try:
+ # gc is disabled, so (assuming no daemonic
+ # threads) the following is the only line in
+ # this function which might trigger creation
+ # of a new finalizer
+ f()
+ except Exception:
+ sys.excepthook(*sys.exc_info())
+ assert f not in cls._registry
+ finally:
+ # prevent any more finalizers from executing during shutdown
+ finalize._shutdown = True
+ if reenable_gc:
+ gc.enable()
diff --git a/mixly/tools/python/click/__init__.py b/mixly/tools/python/click/__init__.py
new file mode 100644
index 00000000..2b6008f2
--- /dev/null
+++ b/mixly/tools/python/click/__init__.py
@@ -0,0 +1,79 @@
+"""
+Click is a simple Python module inspired by the stdlib optparse to make
+writing command line scripts fun. Unlike other modules, it's based
+around a simple API that does not come with too much magic and is
+composable.
+"""
+from .core import Argument
+from .core import BaseCommand
+from .core import Command
+from .core import CommandCollection
+from .core import Context
+from .core import Group
+from .core import MultiCommand
+from .core import Option
+from .core import Parameter
+from .decorators import argument
+from .decorators import command
+from .decorators import confirmation_option
+from .decorators import group
+from .decorators import help_option
+from .decorators import make_pass_decorator
+from .decorators import option
+from .decorators import pass_context
+from .decorators import pass_obj
+from .decorators import password_option
+from .decorators import version_option
+from .exceptions import Abort
+from .exceptions import BadArgumentUsage
+from .exceptions import BadOptionUsage
+from .exceptions import BadParameter
+from .exceptions import ClickException
+from .exceptions import FileError
+from .exceptions import MissingParameter
+from .exceptions import NoSuchOption
+from .exceptions import UsageError
+from .formatting import HelpFormatter
+from .formatting import wrap_text
+from .globals import get_current_context
+from .parser import OptionParser
+from .termui import clear
+from .termui import confirm
+from .termui import echo_via_pager
+from .termui import edit
+from .termui import get_terminal_size
+from .termui import getchar
+from .termui import launch
+from .termui import pause
+from .termui import progressbar
+from .termui import prompt
+from .termui import secho
+from .termui import style
+from .termui import unstyle
+from .types import BOOL
+from .types import Choice
+from .types import DateTime
+from .types import File
+from .types import FLOAT
+from .types import FloatRange
+from .types import INT
+from .types import IntRange
+from .types import ParamType
+from .types import Path
+from .types import STRING
+from .types import Tuple
+from .types import UNPROCESSED
+from .types import UUID
+from .utils import echo
+from .utils import format_filename
+from .utils import get_app_dir
+from .utils import get_binary_stream
+from .utils import get_os_args
+from .utils import get_text_stream
+from .utils import open_file
+
+# Controls if click should emit the warning about the use of unicode
+# literals.
+disable_unicode_literals_warning = False
+
+__version__ = "7.1.2"
diff --git a/mixly/tools/python/click/_bashcomplete.py b/mixly/tools/python/click/_bashcomplete.py
new file mode 100644
index 00000000..8bca2448
--- /dev/null
+++ b/mixly/tools/python/click/_bashcomplete.py
@@ -0,0 +1,375 @@
+import copy
+import os
+import re
+
+from .core import Argument
+from .core import MultiCommand
+from .core import Option
+from .parser import split_arg_string
+from .types import Choice
+from .utils import echo
+
+try:
+ from collections import abc
+except ImportError:
+ import collections as abc
+
+WORDBREAK = "="
+
+# Note, only BASH version 4.4 and later have the nosort option.
+COMPLETION_SCRIPT_BASH = """
+%(complete_func)s() {
+ local IFS=$'\n'
+ COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\
+ COMP_CWORD=$COMP_CWORD \\
+ %(autocomplete_var)s=complete $1 ) )
+ return 0
+}
+
+%(complete_func)setup() {
+ local COMPLETION_OPTIONS=""
+ local BASH_VERSION_ARR=(${BASH_VERSION//./ })
+ # Only BASH version 4.4 and later have the nosort option.
+ if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] \
+&& [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then
+ COMPLETION_OPTIONS="-o nosort"
+ fi
+
+ complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s
+}
+
+%(complete_func)setup
+"""
+
+COMPLETION_SCRIPT_ZSH = """
+#compdef %(script_names)s
+
+%(complete_func)s() {
+ local -a completions
+ local -a completions_with_descriptions
+ local -a response
+ (( ! $+commands[%(script_names)s] )) && return 1
+
+ response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\
+ COMP_CWORD=$((CURRENT-1)) \\
+ %(autocomplete_var)s=\"complete_zsh\" \\
+ %(script_names)s )}")
+
+ for key descr in ${(kv)response}; do
+ if [[ "$descr" == "_" ]]; then
+ completions+=("$key")
+ else
+ completions_with_descriptions+=("$key":"$descr")
+ fi
+ done
+
+ if [ -n "$completions_with_descriptions" ]; then
+ _describe -V unsorted completions_with_descriptions -U
+ fi
+
+ if [ -n "$completions" ]; then
+ compadd -U -V unsorted -a completions
+ fi
+ compstate[insert]="automenu"
+}
+
+compdef %(complete_func)s %(script_names)s
+"""
+
+COMPLETION_SCRIPT_FISH = (
+ "complete --no-files --command %(script_names)s --arguments"
+ ' "(env %(autocomplete_var)s=complete_fish'
+ " COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t)"
+ ' %(script_names)s)"'
+)
+
+_completion_scripts = {
+ "bash": COMPLETION_SCRIPT_BASH,
+ "zsh": COMPLETION_SCRIPT_ZSH,
+ "fish": COMPLETION_SCRIPT_FISH,
+}
+
+_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]")
+
+
+def get_completion_script(prog_name, complete_var, shell):
+ cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_"))
+ script = _completion_scripts.get(shell, COMPLETION_SCRIPT_BASH)
+ return (
+ script
+ % {
+ "complete_func": "_{}_completion".format(cf_name),
+ "script_names": prog_name,
+ "autocomplete_var": complete_var,
+ }
+ ).strip() + ";"
+
+
+def resolve_ctx(cli, prog_name, args):
+ """Parse into a hierarchy of contexts. Contexts are connected
+ through the parent variable.
+
+ :param cli: command definition
+ :param prog_name: the program that is running
+ :param args: full list of args
+ :return: the final context/command parsed
+ """
+ ctx = cli.make_context(prog_name, args, resilient_parsing=True)
+ args = ctx.protected_args + ctx.args
+ while args:
+ if isinstance(ctx.command, MultiCommand):
+ if not ctx.command.chain:
+ cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
+ if cmd is None:
+ return ctx
+ ctx = cmd.make_context(
+ cmd_name, args, parent=ctx, resilient_parsing=True
+ )
+ args = ctx.protected_args + ctx.args
+ else:
+ # Walk chained subcommand contexts saving the last one.
+ while args:
+ cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
+ if cmd is None:
+ return ctx
+ sub_ctx = cmd.make_context(
+ cmd_name,
+ args,
+ parent=ctx,
+ allow_extra_args=True,
+ allow_interspersed_args=False,
+ resilient_parsing=True,
+ )
+ args = sub_ctx.args
+ ctx = sub_ctx
+ args = sub_ctx.protected_args + sub_ctx.args
+ else:
+ break
+ return ctx
+
+
+def start_of_option(param_str):
+ """
+ :param param_str: param_str to check
+ :return: whether or not this is the start of an option declaration
+ (i.e. starts "-" or "--")
+ """
+ return param_str and param_str[:1] == "-"
+
+
+def is_incomplete_option(all_args, cmd_param):
+ """
+ :param all_args: the full original list of args supplied
+ :param cmd_param: the current command paramter
+ :return: whether or not the last option declaration (i.e. starts
+ "-" or "--") is incomplete and corresponds to this cmd_param. In
+ other words whether this cmd_param option can still accept
+ values
+ """
+ if not isinstance(cmd_param, Option):
+ return False
+ if cmd_param.is_flag:
+ return False
+ last_option = None
+ for index, arg_str in enumerate(
+ reversed([arg for arg in all_args if arg != WORDBREAK])
+ ):
+ if index + 1 > cmd_param.nargs:
+ break
+ if start_of_option(arg_str):
+ last_option = arg_str
+
+ return True if last_option and last_option in cmd_param.opts else False
+
+
+def is_incomplete_argument(current_params, cmd_param):
+ """
+ :param current_params: the current params and values for this
+ argument as already entered
+ :param cmd_param: the current command parameter
+ :return: whether or not the last argument is incomplete and
+ corresponds to this cmd_param. In other words whether or not the
+ this cmd_param argument can still accept values
+ """
+ if not isinstance(cmd_param, Argument):
+ return False
+ current_param_values = current_params[cmd_param.name]
+ if current_param_values is None:
+ return True
+ if cmd_param.nargs == -1:
+ return True
+ if (
+ isinstance(current_param_values, abc.Iterable)
+ and cmd_param.nargs > 1
+ and len(current_param_values) < cmd_param.nargs
+ ):
+ return True
+ return False
+
+
+def get_user_autocompletions(ctx, args, incomplete, cmd_param):
+ """
+ :param ctx: context associated with the parsed command
+ :param args: full list of args
+ :param incomplete: the incomplete text to autocomplete
+ :param cmd_param: command definition
+ :return: all the possible user-specified completions for the param
+ """
+ results = []
+ if isinstance(cmd_param.type, Choice):
+ # Choices don't support descriptions.
+ results = [
+ (c, None) for c in cmd_param.type.choices if str(c).startswith(incomplete)
+ ]
+ elif cmd_param.autocompletion is not None:
+ dynamic_completions = cmd_param.autocompletion(
+ ctx=ctx, args=args, incomplete=incomplete
+ )
+ results = [
+ c if isinstance(c, tuple) else (c, None) for c in dynamic_completions
+ ]
+ return results
+
+
+def get_visible_commands_starting_with(ctx, starts_with):
+ """
+ :param ctx: context associated with the parsed command
+ :starts_with: string that visible commands must start with.
+ :return: all visible (not hidden) commands that start with starts_with.
+ """
+ for c in ctx.command.list_commands(ctx):
+ if c.startswith(starts_with):
+ command = ctx.command.get_command(ctx, c)
+ if not command.hidden:
+ yield command
+
+
+def add_subcommand_completions(ctx, incomplete, completions_out):
+ # Add subcommand completions.
+ if isinstance(ctx.command, MultiCommand):
+ completions_out.extend(
+ [
+ (c.name, c.get_short_help_str())
+ for c in get_visible_commands_starting_with(ctx, incomplete)
+ ]
+ )
+
+ # Walk up the context list and add any other completion
+ # possibilities from chained commands
+ while ctx.parent is not None:
+ ctx = ctx.parent
+ if isinstance(ctx.command, MultiCommand) and ctx.command.chain:
+ remaining_commands = [
+ c
+ for c in get_visible_commands_starting_with(ctx, incomplete)
+ if c.name not in ctx.protected_args
+ ]
+ completions_out.extend(
+ [(c.name, c.get_short_help_str()) for c in remaining_commands]
+ )
+
+
+def get_choices(cli, prog_name, args, incomplete):
+ """
+ :param cli: command definition
+ :param prog_name: the program that is running
+ :param args: full list of args
+ :param incomplete: the incomplete text to autocomplete
+ :return: all the possible completions for the incomplete
+ """
+ all_args = copy.deepcopy(args)
+
+ ctx = resolve_ctx(cli, prog_name, args)
+ if ctx is None:
+ return []
+
+ has_double_dash = "--" in all_args
+
+ # In newer versions of bash long opts with '='s are partitioned, but
+ # it's easier to parse without the '='
+ if start_of_option(incomplete) and WORDBREAK in incomplete:
+ partition_incomplete = incomplete.partition(WORDBREAK)
+ all_args.append(partition_incomplete[0])
+ incomplete = partition_incomplete[2]
+ elif incomplete == WORDBREAK:
+ incomplete = ""
+
+ completions = []
+ if not has_double_dash and start_of_option(incomplete):
+ # completions for partial options
+ for param in ctx.command.params:
+ if isinstance(param, Option) and not param.hidden:
+ param_opts = [
+ param_opt
+ for param_opt in param.opts + param.secondary_opts
+ if param_opt not in all_args or param.multiple
+ ]
+ completions.extend(
+ [(o, param.help) for o in param_opts if o.startswith(incomplete)]
+ )
+ return completions
+ # completion for option values from user supplied values
+ for param in ctx.command.params:
+ if is_incomplete_option(all_args, param):
+ return get_user_autocompletions(ctx, all_args, incomplete, param)
+ # completion for argument values from user supplied values
+ for param in ctx.command.params:
+ if is_incomplete_argument(ctx.params, param):
+ return get_user_autocompletions(ctx, all_args, incomplete, param)
+
+ add_subcommand_completions(ctx, incomplete, completions)
+ # Sort before returning so that proper ordering can be enforced in custom types.
+ return sorted(completions)
+
+
+def do_complete(cli, prog_name, include_descriptions):
+ cwords = split_arg_string(os.environ["COMP_WORDS"])
+ cword = int(os.environ["COMP_CWORD"])
+ args = cwords[1:cword]
+ try:
+ incomplete = cwords[cword]
+ except IndexError:
+ incomplete = ""
+
+ for item in get_choices(cli, prog_name, args, incomplete):
+ echo(item[0])
+ if include_descriptions:
+ # ZSH has trouble dealing with empty array parameters when
+ # returned from commands, use '_' to indicate no description
+ # is present.
+ echo(item[1] if item[1] else "_")
+
+ return True
+
+
+def do_complete_fish(cli, prog_name):
+ cwords = split_arg_string(os.environ["COMP_WORDS"])
+ incomplete = os.environ["COMP_CWORD"]
+ args = cwords[1:]
+
+ for item in get_choices(cli, prog_name, args, incomplete):
+ if item[1]:
+ echo("{arg}\t{desc}".format(arg=item[0], desc=item[1]))
+ else:
+ echo(item[0])
+
+ return True
+
+
+def bashcomplete(cli, prog_name, complete_var, complete_instr):
+ if "_" in complete_instr:
+ command, shell = complete_instr.split("_", 1)
+ else:
+ command = complete_instr
+ shell = "bash"
+
+ if command == "source":
+ echo(get_completion_script(prog_name, complete_var, shell))
+ return True
+ elif command == "complete":
+ if shell == "fish":
+ return do_complete_fish(cli, prog_name)
+ elif shell in {"bash", "zsh"}:
+ return do_complete(cli, prog_name, shell == "zsh")
+
+ return False
diff --git a/mixly/tools/python/click/_compat.py b/mixly/tools/python/click/_compat.py
new file mode 100644
index 00000000..60cb115b
--- /dev/null
+++ b/mixly/tools/python/click/_compat.py
@@ -0,0 +1,786 @@
+# flake8: noqa
+import codecs
+import io
+import os
+import re
+import sys
+from weakref import WeakKeyDictionary
+
+PY2 = sys.version_info[0] == 2
+CYGWIN = sys.platform.startswith("cygwin")
+MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version)
+# Determine local App Engine environment, per Google's own suggestion
+APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ.get(
+ "SERVER_SOFTWARE", ""
+)
+WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2
+DEFAULT_COLUMNS = 80
+
+
+_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
+
+
+def get_filesystem_encoding():
+ return sys.getfilesystemencoding() or sys.getdefaultencoding()
+
+
+def _make_text_stream(
+ stream, encoding, errors, force_readable=False, force_writable=False
+):
+ if encoding is None:
+ encoding = get_best_encoding(stream)
+ if errors is None:
+ errors = "replace"
+ return _NonClosingTextIOWrapper(
+ stream,
+ encoding,
+ errors,
+ line_buffering=True,
+ force_readable=force_readable,
+ force_writable=force_writable,
+ )
+
+
+def is_ascii_encoding(encoding):
+ """Checks if a given encoding is ascii."""
+ try:
+ return codecs.lookup(encoding).name == "ascii"
+ except LookupError:
+ return False
+
+
+def get_best_encoding(stream):
+ """Returns the default stream encoding if not found."""
+ rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
+ if is_ascii_encoding(rv):
+ return "utf-8"
+ return rv
+
+
+class _NonClosingTextIOWrapper(io.TextIOWrapper):
+ def __init__(
+ self,
+ stream,
+ encoding,
+ errors,
+ force_readable=False,
+ force_writable=False,
+ **extra
+ ):
+ self._stream = stream = _FixupStream(stream, force_readable, force_writable)
+ io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra)
+
+ # The io module is a place where the Python 3 text behavior
+ # was forced upon Python 2, so we need to unbreak
+ # it to look like Python 2.
+ if PY2:
+
+ def write(self, x):
+ if isinstance(x, str) or is_bytes(x):
+ try:
+ self.flush()
+ except Exception:
+ pass
+ return self.buffer.write(str(x))
+ return io.TextIOWrapper.write(self, x)
+
+ def writelines(self, lines):
+ for line in lines:
+ self.write(line)
+
+ def __del__(self):
+ try:
+ self.detach()
+ except Exception:
+ pass
+
+ def isatty(self):
+ # https://bitbucket.org/pypy/pypy/issue/1803
+ return self._stream.isatty()
+
+
+class _FixupStream(object):
+ """The new io interface needs more from streams than streams
+ traditionally implement. As such, this fix-up code is necessary in
+ some circumstances.
+
+ The forcing of readable and writable flags are there because some tools
+ put badly patched objects on sys (one such offender are certain version
+ of jupyter notebook).
+ """
+
+ def __init__(self, stream, force_readable=False, force_writable=False):
+ self._stream = stream
+ self._force_readable = force_readable
+ self._force_writable = force_writable
+
+ def __getattr__(self, name):
+ return getattr(self._stream, name)
+
+ def read1(self, size):
+ f = getattr(self._stream, "read1", None)
+ if f is not None:
+ return f(size)
+ # We only dispatch to readline instead of read in Python 2 as we
+ # do not want cause problems with the different implementation
+ # of line buffering.
+ if PY2:
+ return self._stream.readline(size)
+ return self._stream.read(size)
+
+ def readable(self):
+ if self._force_readable:
+ return True
+ x = getattr(self._stream, "readable", None)
+ if x is not None:
+ return x()
+ try:
+ self._stream.read(0)
+ except Exception:
+ return False
+ return True
+
+ def writable(self):
+ if self._force_writable:
+ return True
+ x = getattr(self._stream, "writable", None)
+ if x is not None:
+ return x()
+ try:
+ self._stream.write("")
+ except Exception:
+ try:
+ self._stream.write(b"")
+ except Exception:
+ return False
+ return True
+
+ def seekable(self):
+ x = getattr(self._stream, "seekable", None)
+ if x is not None:
+ return x()
+ try:
+ self._stream.seek(self._stream.tell())
+ except Exception:
+ return False
+ return True
+
+
+if PY2:
+ text_type = unicode
+ raw_input = raw_input
+ string_types = (str, unicode)
+ int_types = (int, long)
+ iteritems = lambda x: x.iteritems()
+ range_type = xrange
+
+ def is_bytes(x):
+ return isinstance(x, (buffer, bytearray))
+
+ _identifier_re = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
+
+ # For Windows, we need to force stdout/stdin/stderr to binary if it's
+ # fetched for that. This obviously is not the most correct way to do
+ # it as it changes global state. Unfortunately, there does not seem to
+ # be a clear better way to do it as just reopening the file in binary
+ # mode does not change anything.
+ #
+ # An option would be to do what Python 3 does and to open the file as
+ # binary only, patch it back to the system, and then use a wrapper
+ # stream that converts newlines. It's not quite clear what's the
+ # correct option here.
+ #
+ # This code also lives in _winconsole for the fallback to the console
+ # emulation stream.
+ #
+ # There are also Windows environments where the `msvcrt` module is not
+ # available (which is why we use try-catch instead of the WIN variable
+ # here), such as the Google App Engine development server on Windows. In
+ # those cases there is just nothing we can do.
+ def set_binary_mode(f):
+ return f
+
+ try:
+ import msvcrt
+ except ImportError:
+ pass
+ else:
+
+ def set_binary_mode(f):
+ try:
+ fileno = f.fileno()
+ except Exception:
+ pass
+ else:
+ msvcrt.setmode(fileno, os.O_BINARY)
+ return f
+
+ try:
+ import fcntl
+ except ImportError:
+ pass
+ else:
+
+ def set_binary_mode(f):
+ try:
+ fileno = f.fileno()
+ except Exception:
+ pass
+ else:
+ flags = fcntl.fcntl(fileno, fcntl.F_GETFL)
+ fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
+ return f
+
+ def isidentifier(x):
+ return _identifier_re.search(x) is not None
+
+ def get_binary_stdin():
+ return set_binary_mode(sys.stdin)
+
+ def get_binary_stdout():
+ _wrap_std_stream("stdout")
+ return set_binary_mode(sys.stdout)
+
+ def get_binary_stderr():
+ _wrap_std_stream("stderr")
+ return set_binary_mode(sys.stderr)
+
+ def get_text_stdin(encoding=None, errors=None):
+ rv = _get_windows_console_stream(sys.stdin, encoding, errors)
+ if rv is not None:
+ return rv
+ return _make_text_stream(sys.stdin, encoding, errors, force_readable=True)
+
+ def get_text_stdout(encoding=None, errors=None):
+ _wrap_std_stream("stdout")
+ rv = _get_windows_console_stream(sys.stdout, encoding, errors)
+ if rv is not None:
+ return rv
+ return _make_text_stream(sys.stdout, encoding, errors, force_writable=True)
+
+ def get_text_stderr(encoding=None, errors=None):
+ _wrap_std_stream("stderr")
+ rv = _get_windows_console_stream(sys.stderr, encoding, errors)
+ if rv is not None:
+ return rv
+ return _make_text_stream(sys.stderr, encoding, errors, force_writable=True)
+
+ def filename_to_ui(value):
+ if isinstance(value, bytes):
+ value = value.decode(get_filesystem_encoding(), "replace")
+ return value
+
+
+else:
+ import io
+
+ text_type = str
+ raw_input = input
+ string_types = (str,)
+ int_types = (int,)
+ range_type = range
+ isidentifier = lambda x: x.isidentifier()
+ iteritems = lambda x: iter(x.items())
+
+ def is_bytes(x):
+ return isinstance(x, (bytes, memoryview, bytearray))
+
+ def _is_binary_reader(stream, default=False):
+ try:
+ return isinstance(stream.read(0), bytes)
+ except Exception:
+ return default
+ # This happens in some cases where the stream was already
+ # closed. In this case, we assume the default.
+
+ def _is_binary_writer(stream, default=False):
+ try:
+ stream.write(b"")
+ except Exception:
+ try:
+ stream.write("")
+ return False
+ except Exception:
+ pass
+ return default
+ return True
+
+ def _find_binary_reader(stream):
+ # We need to figure out if the given stream is already binary.
+ # This can happen because the official docs recommend detaching
+ # the streams to get binary streams. Some code might do this, so
+ # we need to deal with this case explicitly.
+ if _is_binary_reader(stream, False):
+ return stream
+
+ buf = getattr(stream, "buffer", None)
+
+ # Same situation here; this time we assume that the buffer is
+ # actually binary in case it's closed.
+ if buf is not None and _is_binary_reader(buf, True):
+ return buf
+
+ def _find_binary_writer(stream):
+ # We need to figure out if the given stream is already binary.
+ # This can happen because the official docs recommend detatching
+ # the streams to get binary streams. Some code might do this, so
+ # we need to deal with this case explicitly.
+ if _is_binary_writer(stream, False):
+ return stream
+
+ buf = getattr(stream, "buffer", None)
+
+ # Same situation here; this time we assume that the buffer is
+ # actually binary in case it's closed.
+ if buf is not None and _is_binary_writer(buf, True):
+ return buf
+
+ def _stream_is_misconfigured(stream):
+ """A stream is misconfigured if its encoding is ASCII."""
+ # If the stream does not have an encoding set, we assume it's set
+ # to ASCII. This appears to happen in certain unittest
+ # environments. It's not quite clear what the correct behavior is
+ # but this at least will force Click to recover somehow.
+ return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
+
+ def _is_compat_stream_attr(stream, attr, value):
+ """A stream attribute is compatible if it is equal to the
+ desired value or the desired value is unset and the attribute
+ has a value.
+ """
+ stream_value = getattr(stream, attr, None)
+ return stream_value == value or (value is None and stream_value is not None)
+
+ def _is_compatible_text_stream(stream, encoding, errors):
+ """Check if a stream's encoding and errors attributes are
+ compatible with the desired values.
+ """
+ return _is_compat_stream_attr(
+ stream, "encoding", encoding
+ ) and _is_compat_stream_attr(stream, "errors", errors)
+
+ def _force_correct_text_stream(
+ text_stream,
+ encoding,
+ errors,
+ is_binary,
+ find_binary,
+ force_readable=False,
+ force_writable=False,
+ ):
+ if is_binary(text_stream, False):
+ binary_reader = text_stream
+ else:
+ # If the stream looks compatible, and won't default to a
+ # misconfigured ascii encoding, return it as-is.
+ if _is_compatible_text_stream(text_stream, encoding, errors) and not (
+ encoding is None and _stream_is_misconfigured(text_stream)
+ ):
+ return text_stream
+
+ # Otherwise, get the underlying binary reader.
+ binary_reader = find_binary(text_stream)
+
+ # If that's not possible, silently use the original reader
+ # and get mojibake instead of exceptions.
+ if binary_reader is None:
+ return text_stream
+
+ # Default errors to replace instead of strict in order to get
+ # something that works.
+ if errors is None:
+ errors = "replace"
+
+ # Wrap the binary stream in a text stream with the correct
+ # encoding parameters.
+ return _make_text_stream(
+ binary_reader,
+ encoding,
+ errors,
+ force_readable=force_readable,
+ force_writable=force_writable,
+ )
+
+ def _force_correct_text_reader(text_reader, encoding, errors, force_readable=False):
+ return _force_correct_text_stream(
+ text_reader,
+ encoding,
+ errors,
+ _is_binary_reader,
+ _find_binary_reader,
+ force_readable=force_readable,
+ )
+
+ def _force_correct_text_writer(text_writer, encoding, errors, force_writable=False):
+ return _force_correct_text_stream(
+ text_writer,
+ encoding,
+ errors,
+ _is_binary_writer,
+ _find_binary_writer,
+ force_writable=force_writable,
+ )
+
+ def get_binary_stdin():
+ reader = _find_binary_reader(sys.stdin)
+ if reader is None:
+ raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
+ return reader
+
+ def get_binary_stdout():
+ writer = _find_binary_writer(sys.stdout)
+ if writer is None:
+ raise RuntimeError(
+ "Was not able to determine binary stream for sys.stdout."
+ )
+ return writer
+
+ def get_binary_stderr():
+ writer = _find_binary_writer(sys.stderr)
+ if writer is None:
+ raise RuntimeError(
+ "Was not able to determine binary stream for sys.stderr."
+ )
+ return writer
+
+ def get_text_stdin(encoding=None, errors=None):
+ rv = _get_windows_console_stream(sys.stdin, encoding, errors)
+ if rv is not None:
+ return rv
+ return _force_correct_text_reader(
+ sys.stdin, encoding, errors, force_readable=True
+ )
+
+ def get_text_stdout(encoding=None, errors=None):
+ rv = _get_windows_console_stream(sys.stdout, encoding, errors)
+ if rv is not None:
+ return rv
+ return _force_correct_text_writer(
+ sys.stdout, encoding, errors, force_writable=True
+ )
+
+ def get_text_stderr(encoding=None, errors=None):
+ rv = _get_windows_console_stream(sys.stderr, encoding, errors)
+ if rv is not None:
+ return rv
+ return _force_correct_text_writer(
+ sys.stderr, encoding, errors, force_writable=True
+ )
+
+ def filename_to_ui(value):
+ if isinstance(value, bytes):
+ value = value.decode(get_filesystem_encoding(), "replace")
+ else:
+ value = value.encode("utf-8", "surrogateescape").decode("utf-8", "replace")
+ return value
+
+
+def get_streerror(e, default=None):
+ if hasattr(e, "strerror"):
+ msg = e.strerror
+ else:
+ if default is not None:
+ msg = default
+ else:
+ msg = str(e)
+ if isinstance(msg, bytes):
+ msg = msg.decode("utf-8", "replace")
+ return msg
+
+
+def _wrap_io_open(file, mode, encoding, errors):
+ """On Python 2, :func:`io.open` returns a text file wrapper that
+ requires passing ``unicode`` to ``write``. Need to open the file in
+ binary mode then wrap it in a subclass that can write ``str`` and
+ ``unicode``.
+
+ Also handles not passing ``encoding`` and ``errors`` in binary mode.
+ """
+ binary = "b" in mode
+
+ if binary:
+ kwargs = {}
+ else:
+ kwargs = {"encoding": encoding, "errors": errors}
+
+ if not PY2 or binary:
+ return io.open(file, mode, **kwargs)
+
+ f = io.open(file, "{}b".format(mode.replace("t", "")))
+ return _make_text_stream(f, **kwargs)
+
+
+def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False):
+ binary = "b" in mode
+
+ # Standard streams first. These are simple because they don't need
+ # special handling for the atomic flag. It's entirely ignored.
+ if filename == "-":
+ if any(m in mode for m in ["w", "a", "x"]):
+ if binary:
+ return get_binary_stdout(), False
+ return get_text_stdout(encoding=encoding, errors=errors), False
+ if binary:
+ return get_binary_stdin(), False
+ return get_text_stdin(encoding=encoding, errors=errors), False
+
+ # Non-atomic writes directly go out through the regular open functions.
+ if not atomic:
+ return _wrap_io_open(filename, mode, encoding, errors), True
+
+ # Some usability stuff for atomic writes
+ if "a" in mode:
+ raise ValueError(
+ "Appending to an existing file is not supported, because that"
+ " would involve an expensive `copy`-operation to a temporary"
+ " file. Open the file in normal `w`-mode and copy explicitly"
+ " if that's what you're after."
+ )
+ if "x" in mode:
+ raise ValueError("Use the `overwrite`-parameter instead.")
+ if "w" not in mode:
+ raise ValueError("Atomic writes only make sense with `w`-mode.")
+
+ # Atomic writes are more complicated. They work by opening a file
+ # as a proxy in the same folder and then using the fdopen
+ # functionality to wrap it in a Python file. Then we wrap it in an
+ # atomic file that moves the file over on close.
+ import errno
+ import random
+
+ try:
+ perm = os.stat(filename).st_mode
+ except OSError:
+ perm = None
+
+ flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
+
+ if binary:
+ flags |= getattr(os, "O_BINARY", 0)
+
+ while True:
+ tmp_filename = os.path.join(
+ os.path.dirname(filename),
+ ".__atomic-write{:08x}".format(random.randrange(1 << 32)),
+ )
+ try:
+ fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
+ break
+ except OSError as e:
+ if e.errno == errno.EEXIST or (
+ os.name == "nt"
+ and e.errno == errno.EACCES
+ and os.path.isdir(e.filename)
+ and os.access(e.filename, os.W_OK)
+ ):
+ continue
+ raise
+
+ if perm is not None:
+ os.chmod(tmp_filename, perm) # in case perm includes bits in umask
+
+ f = _wrap_io_open(fd, mode, encoding, errors)
+ return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True
+
+
+# Used in a destructor call, needs extra protection from interpreter cleanup.
+if hasattr(os, "replace"):
+ _replace = os.replace
+ _can_replace = True
+else:
+ _replace = os.rename
+ _can_replace = not WIN
+
+
+class _AtomicFile(object):
+ def __init__(self, f, tmp_filename, real_filename):
+ self._f = f
+ self._tmp_filename = tmp_filename
+ self._real_filename = real_filename
+ self.closed = False
+
+ @property
+ def name(self):
+ return self._real_filename
+
+ def close(self, delete=False):
+ if self.closed:
+ return
+ self._f.close()
+ if not _can_replace:
+ try:
+ os.remove(self._real_filename)
+ except OSError:
+ pass
+ _replace(self._tmp_filename, self._real_filename)
+ self.closed = True
+
+ def __getattr__(self, name):
+ return getattr(self._f, name)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb):
+ self.close(delete=exc_type is not None)
+
+ def __repr__(self):
+ return repr(self._f)
+
+
+auto_wrap_for_ansi = None
+colorama = None
+get_winterm_size = None
+
+
+def strip_ansi(value):
+ return _ansi_re.sub("", value)
+
+
+def _is_jupyter_kernel_output(stream):
+ if WIN:
+ # TODO: Couldn't test on Windows, should't try to support until
+ # someone tests the details wrt colorama.
+ return
+
+ while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
+ stream = stream._stream
+
+ return stream.__class__.__module__.startswith("ipykernel.")
+
+
+def should_strip_ansi(stream=None, color=None):
+ if color is None:
+ if stream is None:
+ stream = sys.stdin
+ return not isatty(stream) and not _is_jupyter_kernel_output(stream)
+ return not color
+
+
+# If we're on Windows, we provide transparent integration through
+# colorama. This will make ANSI colors through the echo function
+# work automatically.
+if WIN:
+ # Windows has a smaller terminal
+ DEFAULT_COLUMNS = 79
+
+ from ._winconsole import _get_windows_console_stream, _wrap_std_stream
+
+ def _get_argv_encoding():
+ import locale
+
+ return locale.getpreferredencoding()
+
+ if PY2:
+
+ def raw_input(prompt=""):
+ sys.stderr.flush()
+ if prompt:
+ stdout = _default_text_stdout()
+ stdout.write(prompt)
+ stdin = _default_text_stdin()
+ return stdin.readline().rstrip("\r\n")
+
+ try:
+ import colorama
+ except ImportError:
+ pass
+ else:
+ _ansi_stream_wrappers = WeakKeyDictionary()
+
+ def auto_wrap_for_ansi(stream, color=None):
+ """This function wraps a stream so that calls through colorama
+ are issued to the win32 console API to recolor on demand. It
+ also ensures to reset the colors if a write call is interrupted
+ to not destroy the console afterwards.
+ """
+ try:
+ cached = _ansi_stream_wrappers.get(stream)
+ except Exception:
+ cached = None
+ if cached is not None:
+ return cached
+ strip = should_strip_ansi(stream, color)
+ ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
+ rv = ansi_wrapper.stream
+ _write = rv.write
+
+ def _safe_write(s):
+ try:
+ return _write(s)
+ except:
+ ansi_wrapper.reset_all()
+ raise
+
+ rv.write = _safe_write
+ try:
+ _ansi_stream_wrappers[stream] = rv
+ except Exception:
+ pass
+ return rv
+
+ def get_winterm_size():
+ win = colorama.win32.GetConsoleScreenBufferInfo(
+ colorama.win32.STDOUT
+ ).srWindow
+ return win.Right - win.Left, win.Bottom - win.Top
+
+
+else:
+
+ def _get_argv_encoding():
+ return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding()
+
+ _get_windows_console_stream = lambda *x: None
+ _wrap_std_stream = lambda *x: None
+
+
+def term_len(x):
+ return len(strip_ansi(x))
+
+
+def isatty(stream):
+ try:
+ return stream.isatty()
+ except Exception:
+ return False
+
+
+def _make_cached_stream_func(src_func, wrapper_func):
+ cache = WeakKeyDictionary()
+
+ def func():
+ stream = src_func()
+ try:
+ rv = cache.get(stream)
+ except Exception:
+ rv = None
+ if rv is not None:
+ return rv
+ rv = wrapper_func()
+ try:
+ stream = src_func() # In case wrapper_func() modified the stream
+ cache[stream] = rv
+ except Exception:
+ pass
+ return rv
+
+ return func
+
+
+_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin)
+_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout)
+_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
+
+
+binary_streams = {
+ "stdin": get_binary_stdin,
+ "stdout": get_binary_stdout,
+ "stderr": get_binary_stderr,
+}
+
+text_streams = {
+ "stdin": get_text_stdin,
+ "stdout": get_text_stdout,
+ "stderr": get_text_stderr,
+}
diff --git a/mixly/tools/python/click/_termui_impl.py b/mixly/tools/python/click/_termui_impl.py
new file mode 100644
index 00000000..88bec377
--- /dev/null
+++ b/mixly/tools/python/click/_termui_impl.py
@@ -0,0 +1,657 @@
+# -*- coding: utf-8 -*-
+"""
+This module contains implementations for the termui module. To keep the
+import time of Click down, some infrequently used functionality is
+placed in this module and only imported as needed.
+"""
+import contextlib
+import math
+import os
+import sys
+import time
+
+from ._compat import _default_text_stdout
+from ._compat import CYGWIN
+from ._compat import get_best_encoding
+from ._compat import int_types
+from ._compat import isatty
+from ._compat import open_stream
+from ._compat import range_type
+from ._compat import strip_ansi
+from ._compat import term_len
+from ._compat import WIN
+from .exceptions import ClickException
+from .utils import echo
+
+if os.name == "nt":
+ BEFORE_BAR = "\r"
+ AFTER_BAR = "\n"
+else:
+ BEFORE_BAR = "\r\033[?25l"
+ AFTER_BAR = "\033[?25h\n"
+
+
+def _length_hint(obj):
+ """Returns the length hint of an object."""
+ try:
+ return len(obj)
+ except (AttributeError, TypeError):
+ try:
+ get_hint = type(obj).__length_hint__
+ except AttributeError:
+ return None
+ try:
+ hint = get_hint(obj)
+ except TypeError:
+ return None
+ if hint is NotImplemented or not isinstance(hint, int_types) or hint < 0:
+ return None
+ return hint
+
+
+class ProgressBar(object):
+ def __init__(
+ self,
+ iterable,
+ length=None,
+ fill_char="#",
+ empty_char=" ",
+ bar_template="%(bar)s",
+ info_sep=" ",
+ show_eta=True,
+ show_percent=None,
+ show_pos=False,
+ item_show_func=None,
+ label=None,
+ file=None,
+ color=None,
+ width=30,
+ ):
+ self.fill_char = fill_char
+ self.empty_char = empty_char
+ self.bar_template = bar_template
+ self.info_sep = info_sep
+ self.show_eta = show_eta
+ self.show_percent = show_percent
+ self.show_pos = show_pos
+ self.item_show_func = item_show_func
+ self.label = label or ""
+ if file is None:
+ file = _default_text_stdout()
+ self.file = file
+ self.color = color
+ self.width = width
+ self.autowidth = width == 0
+
+ if length is None:
+ length = _length_hint(iterable)
+ if iterable is None:
+ if length is None:
+ raise TypeError("iterable or length is required")
+ iterable = range_type(length)
+ self.iter = iter(iterable)
+ self.length = length
+ self.length_known = length is not None
+ self.pos = 0
+ self.avg = []
+ self.start = self.last_eta = time.time()
+ self.eta_known = False
+ self.finished = False
+ self.max_width = None
+ self.entered = False
+ self.current_item = None
+ self.is_hidden = not isatty(self.file)
+ self._last_line = None
+ self.short_limit = 0.5
+
+ def __enter__(self):
+ self.entered = True
+ self.render_progress()
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb):
+ self.render_finish()
+
+ def __iter__(self):
+ if not self.entered:
+ raise RuntimeError("You need to use progress bars in a with block.")
+ self.render_progress()
+ return self.generator()
+
+ def __next__(self):
+ # Iteration is defined in terms of a generator function,
+ # returned by iter(self); use that to define next(). This works
+ # because `self.iter` is an iterable consumed by that generator,
+ # so it is re-entry safe. Calling `next(self.generator())`
+ # twice works and does "what you want".
+ return next(iter(self))
+
+ # Python 2 compat
+ next = __next__
+
+ def is_fast(self):
+ return time.time() - self.start <= self.short_limit
+
+ def render_finish(self):
+ if self.is_hidden or self.is_fast():
+ return
+ self.file.write(AFTER_BAR)
+ self.file.flush()
+
+ @property
+ def pct(self):
+ if self.finished:
+ return 1.0
+ return min(self.pos / (float(self.length) or 1), 1.0)
+
+ @property
+ def time_per_iteration(self):
+ if not self.avg:
+ return 0.0
+ return sum(self.avg) / float(len(self.avg))
+
+ @property
+ def eta(self):
+ if self.length_known and not self.finished:
+ return self.time_per_iteration * (self.length - self.pos)
+ return 0.0
+
+ def format_eta(self):
+ if self.eta_known:
+ t = int(self.eta)
+ seconds = t % 60
+ t //= 60
+ minutes = t % 60
+ t //= 60
+ hours = t % 24
+ t //= 24
+ if t > 0:
+ return "{}d {:02}:{:02}:{:02}".format(t, hours, minutes, seconds)
+ else:
+ return "{:02}:{:02}:{:02}".format(hours, minutes, seconds)
+ return ""
+
+ def format_pos(self):
+ pos = str(self.pos)
+ if self.length_known:
+ pos += "/{}".format(self.length)
+ return pos
+
+ def format_pct(self):
+ return "{: 4}%".format(int(self.pct * 100))[1:]
+
+ def format_bar(self):
+ if self.length_known:
+ bar_length = int(self.pct * self.width)
+ bar = self.fill_char * bar_length
+ bar += self.empty_char * (self.width - bar_length)
+ elif self.finished:
+ bar = self.fill_char * self.width
+ else:
+ bar = list(self.empty_char * (self.width or 1))
+ if self.time_per_iteration != 0:
+ bar[
+ int(
+ (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
+ * self.width
+ )
+ ] = self.fill_char
+ bar = "".join(bar)
+ return bar
+
+ def format_progress_line(self):
+ show_percent = self.show_percent
+
+ info_bits = []
+ if self.length_known and show_percent is None:
+ show_percent = not self.show_pos
+
+ if self.show_pos:
+ info_bits.append(self.format_pos())
+ if show_percent:
+ info_bits.append(self.format_pct())
+ if self.show_eta and self.eta_known and not self.finished:
+ info_bits.append(self.format_eta())
+ if self.item_show_func is not None:
+ item_info = self.item_show_func(self.current_item)
+ if item_info is not None:
+ info_bits.append(item_info)
+
+ return (
+ self.bar_template
+ % {
+ "label": self.label,
+ "bar": self.format_bar(),
+ "info": self.info_sep.join(info_bits),
+ }
+ ).rstrip()
+
+ def render_progress(self):
+ from .termui import get_terminal_size
+
+ if self.is_hidden:
+ return
+
+ buf = []
+ # Update width in case the terminal has been resized
+ if self.autowidth:
+ old_width = self.width
+ self.width = 0
+ clutter_length = term_len(self.format_progress_line())
+ new_width = max(0, get_terminal_size()[0] - clutter_length)
+ if new_width < old_width:
+ buf.append(BEFORE_BAR)
+ buf.append(" " * self.max_width)
+ self.max_width = new_width
+ self.width = new_width
+
+ clear_width = self.width
+ if self.max_width is not None:
+ clear_width = self.max_width
+
+ buf.append(BEFORE_BAR)
+ line = self.format_progress_line()
+ line_len = term_len(line)
+ if self.max_width is None or self.max_width < line_len:
+ self.max_width = line_len
+
+ buf.append(line)
+ buf.append(" " * (clear_width - line_len))
+ line = "".join(buf)
+ # Render the line only if it changed.
+
+ if line != self._last_line and not self.is_fast():
+ self._last_line = line
+ echo(line, file=self.file, color=self.color, nl=False)
+ self.file.flush()
+
+ def make_step(self, n_steps):
+ self.pos += n_steps
+ if self.length_known and self.pos >= self.length:
+ self.finished = True
+
+ if (time.time() - self.last_eta) < 1.0:
+ return
+
+ self.last_eta = time.time()
+
+ # self.avg is a rolling list of length <= 7 of steps where steps are
+ # defined as time elapsed divided by the total progress through
+ # self.length.
+ if self.pos:
+ step = (time.time() - self.start) / self.pos
+ else:
+ step = time.time() - self.start
+
+ self.avg = self.avg[-6:] + [step]
+
+ self.eta_known = self.length_known
+
+ def update(self, n_steps):
+ self.make_step(n_steps)
+ self.render_progress()
+
+ def finish(self):
+ self.eta_known = 0
+ self.current_item = None
+ self.finished = True
+
+ def generator(self):
+ """Return a generator which yields the items added to the bar
+ during construction, and updates the progress bar *after* the
+ yielded block returns.
+ """
+ # WARNING: the iterator interface for `ProgressBar` relies on
+ # this and only works because this is a simple generator which
+ # doesn't create or manage additional state. If this function
+ # changes, the impact should be evaluated both against
+ # `iter(bar)` and `next(bar)`. `next()` in particular may call
+ # `self.generator()` repeatedly, and this must remain safe in
+ # order for that interface to work.
+ if not self.entered:
+ raise RuntimeError("You need to use progress bars in a with block.")
+
+ if self.is_hidden:
+ for rv in self.iter:
+ yield rv
+ else:
+ for rv in self.iter:
+ self.current_item = rv
+ yield rv
+ self.update(1)
+ self.finish()
+ self.render_progress()
+
+
+def pager(generator, color=None):
+ """Decide what method to use for paging through text."""
+ stdout = _default_text_stdout()
+ if not isatty(sys.stdin) or not isatty(stdout):
+ return _nullpager(stdout, generator, color)
+ pager_cmd = (os.environ.get("PAGER", None) or "").strip()
+ if pager_cmd:
+ if WIN:
+ return _tempfilepager(generator, pager_cmd, color)
+ return _pipepager(generator, pager_cmd, color)
+ if os.environ.get("TERM") in ("dumb", "emacs"):
+ return _nullpager(stdout, generator, color)
+ if WIN or sys.platform.startswith("os2"):
+ return _tempfilepager(generator, "more <", color)
+ if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0:
+ return _pipepager(generator, "less", color)
+
+ import tempfile
+
+ fd, filename = tempfile.mkstemp()
+ os.close(fd)
+ try:
+ if hasattr(os, "system") and os.system('more "{}"'.format(filename)) == 0:
+ return _pipepager(generator, "more", color)
+ return _nullpager(stdout, generator, color)
+ finally:
+ os.unlink(filename)
+
+
+def _pipepager(generator, cmd, color):
+ """Page through text by feeding it to another program. Invoking a
+ pager through this might support colors.
+ """
+ import subprocess
+
+ env = dict(os.environ)
+
+ # If we're piping to less we might support colors under the
+ # condition that
+ cmd_detail = cmd.rsplit("/", 1)[-1].split()
+ if color is None and cmd_detail[0] == "less":
+ less_flags = "{}{}".format(os.environ.get("LESS", ""), " ".join(cmd_detail[1:]))
+ if not less_flags:
+ env["LESS"] = "-R"
+ color = True
+ elif "r" in less_flags or "R" in less_flags:
+ color = True
+
+ c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env)
+ encoding = get_best_encoding(c.stdin)
+ try:
+ for text in generator:
+ if not color:
+ text = strip_ansi(text)
+
+ c.stdin.write(text.encode(encoding, "replace"))
+ except (IOError, KeyboardInterrupt):
+ pass
+ else:
+ c.stdin.close()
+
+ # Less doesn't respect ^C, but catches it for its own UI purposes (aborting
+ # search or other commands inside less).
+ #
+ # That means when the user hits ^C, the parent process (click) terminates,
+ # but less is still alive, paging the output and messing up the terminal.
+ #
+ # If the user wants to make the pager exit on ^C, they should set
+ # `LESS='-K'`. It's not our decision to make.
+ while True:
+ try:
+ c.wait()
+ except KeyboardInterrupt:
+ pass
+ else:
+ break
+
+
+def _tempfilepager(generator, cmd, color):
+ """Page through text by invoking a program on a temporary file."""
+ import tempfile
+
+ filename = tempfile.mktemp()
+ # TODO: This never terminates if the passed generator never terminates.
+ text = "".join(generator)
+ if not color:
+ text = strip_ansi(text)
+ encoding = get_best_encoding(sys.stdout)
+ with open_stream(filename, "wb")[0] as f:
+ f.write(text.encode(encoding))
+ try:
+ os.system('{} "{}"'.format(cmd, filename))
+ finally:
+ os.unlink(filename)
+
+
+def _nullpager(stream, generator, color):
+ """Simply print unformatted text. This is the ultimate fallback."""
+ for text in generator:
+ if not color:
+ text = strip_ansi(text)
+ stream.write(text)
+
+
+class Editor(object):
+ def __init__(self, editor=None, env=None, require_save=True, extension=".txt"):
+ self.editor = editor
+ self.env = env
+ self.require_save = require_save
+ self.extension = extension
+
+ def get_editor(self):
+ if self.editor is not None:
+ return self.editor
+ for key in "VISUAL", "EDITOR":
+ rv = os.environ.get(key)
+ if rv:
+ return rv
+ if WIN:
+ return "notepad"
+ for editor in "sensible-editor", "vim", "nano":
+ if os.system("which {} >/dev/null 2>&1".format(editor)) == 0:
+ return editor
+ return "vi"
+
+ def edit_file(self, filename):
+ import subprocess
+
+ editor = self.get_editor()
+ if self.env:
+ environ = os.environ.copy()
+ environ.update(self.env)
+ else:
+ environ = None
+ try:
+ c = subprocess.Popen(
+ '{} "{}"'.format(editor, filename), env=environ, shell=True,
+ )
+ exit_code = c.wait()
+ if exit_code != 0:
+ raise ClickException("{}: Editing failed!".format(editor))
+ except OSError as e:
+ raise ClickException("{}: Editing failed: {}".format(editor, e))
+
+ def edit(self, text):
+ import tempfile
+
+ text = text or ""
+ if text and not text.endswith("\n"):
+ text += "\n"
+
+ fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
+ try:
+ if WIN:
+ encoding = "utf-8-sig"
+ text = text.replace("\n", "\r\n")
+ else:
+ encoding = "utf-8"
+ text = text.encode(encoding)
+
+ f = os.fdopen(fd, "wb")
+ f.write(text)
+ f.close()
+ timestamp = os.path.getmtime(name)
+
+ self.edit_file(name)
+
+ if self.require_save and os.path.getmtime(name) == timestamp:
+ return None
+
+ f = open(name, "rb")
+ try:
+ rv = f.read()
+ finally:
+ f.close()
+ return rv.decode("utf-8-sig").replace("\r\n", "\n")
+ finally:
+ os.unlink(name)
+
+
+def open_url(url, wait=False, locate=False):
+ import subprocess
+
+ def _unquote_file(url):
+ try:
+ import urllib
+ except ImportError:
+ import urllib
+ if url.startswith("file://"):
+ url = urllib.unquote(url[7:])
+ return url
+
+ if sys.platform == "darwin":
+ args = ["open"]
+ if wait:
+ args.append("-W")
+ if locate:
+ args.append("-R")
+ args.append(_unquote_file(url))
+ null = open("/dev/null", "w")
+ try:
+ return subprocess.Popen(args, stderr=null).wait()
+ finally:
+ null.close()
+ elif WIN:
+ if locate:
+ url = _unquote_file(url)
+ args = 'explorer /select,"{}"'.format(_unquote_file(url.replace('"', "")))
+ else:
+ args = 'start {} "" "{}"'.format(
+ "/WAIT" if wait else "", url.replace('"', "")
+ )
+ return os.system(args)
+ elif CYGWIN:
+ if locate:
+ url = _unquote_file(url)
+ args = 'cygstart "{}"'.format(os.path.dirname(url).replace('"', ""))
+ else:
+ args = 'cygstart {} "{}"'.format("-w" if wait else "", url.replace('"', ""))
+ return os.system(args)
+
+ try:
+ if locate:
+ url = os.path.dirname(_unquote_file(url)) or "."
+ else:
+ url = _unquote_file(url)
+ c = subprocess.Popen(["xdg-open", url])
+ if wait:
+ return c.wait()
+ return 0
+ except OSError:
+ if url.startswith(("http://", "https://")) and not locate and not wait:
+ import webbrowser
+
+ webbrowser.open(url)
+ return 0
+ return 1
+
+
+def _translate_ch_to_exc(ch):
+ if ch == u"\x03":
+ raise KeyboardInterrupt()
+ if ch == u"\x04" and not WIN: # Unix-like, Ctrl+D
+ raise EOFError()
+ if ch == u"\x1a" and WIN: # Windows, Ctrl+Z
+ raise EOFError()
+
+
+if WIN:
+ import msvcrt
+
+ @contextlib.contextmanager
+ def raw_terminal():
+ yield
+
+ def getchar(echo):
+ # The function `getch` will return a bytes object corresponding to
+ # the pressed character. Since Windows 10 build 1803, it will also
+ # return \x00 when called a second time after pressing a regular key.
+ #
+ # `getwch` does not share this probably-bugged behavior. Moreover, it
+ # returns a Unicode object by default, which is what we want.
+ #
+ # Either of these functions will return \x00 or \xe0 to indicate
+ # a special key, and you need to call the same function again to get
+ # the "rest" of the code. The fun part is that \u00e0 is
+ # "latin small letter a with grave", so if you type that on a French
+ # keyboard, you _also_ get a \xe0.
+ # E.g., consider the Up arrow. This returns \xe0 and then \x48. The
+ # resulting Unicode string reads as "a with grave" + "capital H".
+ # This is indistinguishable from when the user actually types
+ # "a with grave" and then "capital H".
+ #
+ # When \xe0 is returned, we assume it's part of a special-key sequence
+ # and call `getwch` again, but that means that when the user types
+ # the \u00e0 character, `getchar` doesn't return until a second
+ # character is typed.
+ # The alternative is returning immediately, but that would mess up
+ # cross-platform handling of arrow keys and others that start with
+ # \xe0. Another option is using `getch`, but then we can't reliably
+ # read non-ASCII characters, because return values of `getch` are
+ # limited to the current 8-bit codepage.
+ #
+ # Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
+ # is doing the right thing in more situations than with `getch`.
+ if echo:
+ func = msvcrt.getwche
+ else:
+ func = msvcrt.getwch
+
+ rv = func()
+ if rv in (u"\x00", u"\xe0"):
+ # \x00 and \xe0 are control characters that indicate special key,
+ # see above.
+ rv += func()
+ _translate_ch_to_exc(rv)
+ return rv
+
+
+else:
+ import tty
+ import termios
+
+ @contextlib.contextmanager
+ def raw_terminal():
+ if not isatty(sys.stdin):
+ f = open("/dev/tty")
+ fd = f.fileno()
+ else:
+ fd = sys.stdin.fileno()
+ f = None
+ try:
+ old_settings = termios.tcgetattr(fd)
+ try:
+ tty.setraw(fd)
+ yield fd
+ finally:
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+ sys.stdout.flush()
+ if f is not None:
+ f.close()
+ except termios.error:
+ pass
+
+ def getchar(echo):
+ with raw_terminal() as fd:
+ ch = os.read(fd, 32)
+ ch = ch.decode(get_best_encoding(sys.stdin), "replace")
+ if echo and isatty(sys.stdout):
+ sys.stdout.write(ch)
+ _translate_ch_to_exc(ch)
+ return ch
diff --git a/mixly/tools/python/click/_textwrap.py b/mixly/tools/python/click/_textwrap.py
new file mode 100644
index 00000000..6959087b
--- /dev/null
+++ b/mixly/tools/python/click/_textwrap.py
@@ -0,0 +1,37 @@
+import textwrap
+from contextlib import contextmanager
+
+
+class TextWrapper(textwrap.TextWrapper):
+ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
+ space_left = max(width - cur_len, 1)
+
+ if self.break_long_words:
+ last = reversed_chunks[-1]
+ cut = last[:space_left]
+ res = last[space_left:]
+ cur_line.append(cut)
+ reversed_chunks[-1] = res
+ elif not cur_line:
+ cur_line.append(reversed_chunks.pop())
+
+ @contextmanager
+ def extra_indent(self, indent):
+ old_initial_indent = self.initial_indent
+ old_subsequent_indent = self.subsequent_indent
+ self.initial_indent += indent
+ self.subsequent_indent += indent
+ try:
+ yield
+ finally:
+ self.initial_indent = old_initial_indent
+ self.subsequent_indent = old_subsequent_indent
+
+ def indent_only(self, text):
+ rv = []
+ for idx, line in enumerate(text.splitlines()):
+ indent = self.initial_indent
+ if idx > 0:
+ indent = self.subsequent_indent
+ rv.append(indent + line)
+ return "\n".join(rv)
diff --git a/mixly/tools/python/click/_unicodefun.py b/mixly/tools/python/click/_unicodefun.py
new file mode 100644
index 00000000..781c3652
--- /dev/null
+++ b/mixly/tools/python/click/_unicodefun.py
@@ -0,0 +1,131 @@
+import codecs
+import os
+import sys
+
+from ._compat import PY2
+
+
+def _find_unicode_literals_frame():
+ import __future__
+
+ if not hasattr(sys, "_getframe"): # not all Python implementations have it
+ return 0
+ frm = sys._getframe(1)
+ idx = 1
+ while frm is not None:
+ if frm.f_globals.get("__name__", "").startswith("click."):
+ frm = frm.f_back
+ idx += 1
+ elif frm.f_code.co_flags & __future__.unicode_literals.compiler_flag:
+ return idx
+ else:
+ break
+ return 0
+
+
+def _check_for_unicode_literals():
+ if not __debug__:
+ return
+
+ from . import disable_unicode_literals_warning
+
+ if not PY2 or disable_unicode_literals_warning:
+ return
+ bad_frame = _find_unicode_literals_frame()
+ if bad_frame <= 0:
+ return
+ from warnings import warn
+
+ warn(
+ Warning(
+ "Click detected the use of the unicode_literals __future__"
+ " import. This is heavily discouraged because it can"
+ " introduce subtle bugs in your code. You should instead"
+ ' use explicit u"" literals for your unicode strings. For'
+ " more information see"
+ " https://click.palletsprojects.com/python3/"
+ ),
+ stacklevel=bad_frame,
+ )
+
+
+def _verify_python3_env():
+ """Ensures that the environment is good for unicode on Python 3."""
+ if PY2:
+ return
+ try:
+ import locale
+
+ fs_enc = codecs.lookup(locale.getpreferredencoding()).name
+ except Exception:
+ fs_enc = "ascii"
+ if fs_enc != "ascii":
+ return
+
+ extra = ""
+ if os.name == "posix":
+ import subprocess
+
+ try:
+ rv = subprocess.Popen(
+ ["locale", "-a"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ ).communicate()[0]
+ except OSError:
+ rv = b""
+ good_locales = set()
+ has_c_utf8 = False
+
+ # Make sure we're operating on text here.
+ if isinstance(rv, bytes):
+ rv = rv.decode("ascii", "replace")
+
+ for line in rv.splitlines():
+ locale = line.strip()
+ if locale.lower().endswith((".utf-8", ".utf8")):
+ good_locales.add(locale)
+ if locale.lower() in ("c.utf8", "c.utf-8"):
+ has_c_utf8 = True
+
+ extra += "\n\n"
+ if not good_locales:
+ extra += (
+ "Additional information: on this system no suitable"
+ " UTF-8 locales were discovered. This most likely"
+ " requires resolving by reconfiguring the locale"
+ " system."
+ )
+ elif has_c_utf8:
+ extra += (
+ "This system supports the C.UTF-8 locale which is"
+ " recommended. You might be able to resolve your issue"
+ " by exporting the following environment variables:\n\n"
+ " export LC_ALL=C.UTF-8\n"
+ " export LANG=C.UTF-8"
+ )
+ else:
+ extra += (
+ "This system lists a couple of UTF-8 supporting locales"
+ " that you can pick from. The following suitable"
+ " locales were discovered: {}".format(", ".join(sorted(good_locales)))
+ )
+
+ bad_locale = None
+ for locale in os.environ.get("LC_ALL"), os.environ.get("LANG"):
+ if locale and locale.lower().endswith((".utf-8", ".utf8")):
+ bad_locale = locale
+ if locale is not None:
+ break
+ if bad_locale is not None:
+ extra += (
+ "\n\nClick discovered that you exported a UTF-8 locale"
+ " but the locale system could not pick up from it"
+ " because it does not exist. The exported locale is"
+ " '{}' but it is not supported".format(bad_locale)
+ )
+
+ raise RuntimeError(
+ "Click will abort further execution because Python 3 was"
+ " configured to use ASCII as encoding for the environment."
+ " Consult https://click.palletsprojects.com/python3/ for"
+ " mitigation steps.{}".format(extra)
+ )
diff --git a/mixly/tools/python/click/_winconsole.py b/mixly/tools/python/click/_winconsole.py
new file mode 100644
index 00000000..b6c4274a
--- /dev/null
+++ b/mixly/tools/python/click/_winconsole.py
@@ -0,0 +1,370 @@
+# -*- coding: utf-8 -*-
+# This module is based on the excellent work by Adam Bartoš who
+# provided a lot of what went into the implementation here in
+# the discussion to issue1602 in the Python bug tracker.
+#
+# There are some general differences in regards to how this works
+# compared to the original patches as we do not need to patch
+# the entire interpreter but just work in our little world of
+# echo and prmopt.
+import ctypes
+import io
+import os
+import sys
+import time
+import zlib
+from ctypes import byref
+from ctypes import c_char
+from ctypes import c_char_p
+from ctypes import c_int
+from ctypes import c_ssize_t
+from ctypes import c_ulong
+from ctypes import c_void_p
+from ctypes import POINTER
+from ctypes import py_object
+from ctypes import windll
+from ctypes import WinError
+from ctypes import WINFUNCTYPE
+from ctypes.wintypes import DWORD
+from ctypes.wintypes import HANDLE
+from ctypes.wintypes import LPCWSTR
+from ctypes.wintypes import LPWSTR
+
+import msvcrt
+
+from ._compat import _NonClosingTextIOWrapper
+from ._compat import PY2
+from ._compat import text_type
+
+try:
+ from ctypes import pythonapi
+
+ PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
+ PyBuffer_Release = pythonapi.PyBuffer_Release
+except ImportError:
+ pythonapi = None
+
+
+c_ssize_p = POINTER(c_ssize_t)
+
+kernel32 = windll.kernel32
+GetStdHandle = kernel32.GetStdHandle
+ReadConsoleW = kernel32.ReadConsoleW
+WriteConsoleW = kernel32.WriteConsoleW
+GetConsoleMode = kernel32.GetConsoleMode
+GetLastError = kernel32.GetLastError
+GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
+CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
+ ("CommandLineToArgvW", windll.shell32)
+)
+LocalFree = WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)(
+ ("LocalFree", windll.kernel32)
+)
+
+
+STDIN_HANDLE = GetStdHandle(-10)
+STDOUT_HANDLE = GetStdHandle(-11)
+STDERR_HANDLE = GetStdHandle(-12)
+
+
+PyBUF_SIMPLE = 0
+PyBUF_WRITABLE = 1
+
+ERROR_SUCCESS = 0
+ERROR_NOT_ENOUGH_MEMORY = 8
+ERROR_OPERATION_ABORTED = 995
+
+STDIN_FILENO = 0
+STDOUT_FILENO = 1
+STDERR_FILENO = 2
+
+EOF = b"\x1a"
+MAX_BYTES_WRITTEN = 32767
+
+
+class Py_buffer(ctypes.Structure):
+ _fields_ = [
+ ("buf", c_void_p),
+ ("obj", py_object),
+ ("len", c_ssize_t),
+ ("itemsize", c_ssize_t),
+ ("readonly", c_int),
+ ("ndim", c_int),
+ ("format", c_char_p),
+ ("shape", c_ssize_p),
+ ("strides", c_ssize_p),
+ ("suboffsets", c_ssize_p),
+ ("internal", c_void_p),
+ ]
+
+ if PY2:
+ _fields_.insert(-1, ("smalltable", c_ssize_t * 2))
+
+
+# On PyPy we cannot get buffers so our ability to operate here is
+# serverly limited.
+if pythonapi is None:
+ get_buffer = None
+else:
+
+ def get_buffer(obj, writable=False):
+ buf = Py_buffer()
+ flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
+ PyObject_GetBuffer(py_object(obj), byref(buf), flags)
+ try:
+ buffer_type = c_char * buf.len
+ return buffer_type.from_address(buf.buf)
+ finally:
+ PyBuffer_Release(byref(buf))
+
+
+class _WindowsConsoleRawIOBase(io.RawIOBase):
+ def __init__(self, handle):
+ self.handle = handle
+
+ def isatty(self):
+ io.RawIOBase.isatty(self)
+ return True
+
+
+class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
+ def readable(self):
+ return True
+
+ def readinto(self, b):
+ bytes_to_be_read = len(b)
+ if not bytes_to_be_read:
+ return 0
+ elif bytes_to_be_read % 2:
+ raise ValueError(
+ "cannot read odd number of bytes from UTF-16-LE encoded console"
+ )
+
+ buffer = get_buffer(b, writable=True)
+ code_units_to_be_read = bytes_to_be_read // 2
+ code_units_read = c_ulong()
+
+ rv = ReadConsoleW(
+ HANDLE(self.handle),
+ buffer,
+ code_units_to_be_read,
+ byref(code_units_read),
+ None,
+ )
+ if GetLastError() == ERROR_OPERATION_ABORTED:
+ # wait for KeyboardInterrupt
+ time.sleep(0.1)
+ if not rv:
+ raise OSError("Windows error: {}".format(GetLastError()))
+
+ if buffer[0] == EOF:
+ return 0
+ return 2 * code_units_read.value
+
+
+class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
+ def writable(self):
+ return True
+
+ @staticmethod
+ def _get_error_message(errno):
+ if errno == ERROR_SUCCESS:
+ return "ERROR_SUCCESS"
+ elif errno == ERROR_NOT_ENOUGH_MEMORY:
+ return "ERROR_NOT_ENOUGH_MEMORY"
+ return "Windows error {}".format(errno)
+
+ def write(self, b):
+ bytes_to_be_written = len(b)
+ buf = get_buffer(b)
+ code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2
+ code_units_written = c_ulong()
+
+ WriteConsoleW(
+ HANDLE(self.handle),
+ buf,
+ code_units_to_be_written,
+ byref(code_units_written),
+ None,
+ )
+ bytes_written = 2 * code_units_written.value
+
+ if bytes_written == 0 and bytes_to_be_written > 0:
+ raise OSError(self._get_error_message(GetLastError()))
+ return bytes_written
+
+
+class ConsoleStream(object):
+ def __init__(self, text_stream, byte_stream):
+ self._text_stream = text_stream
+ self.buffer = byte_stream
+
+ @property
+ def name(self):
+ return self.buffer.name
+
+ def write(self, x):
+ if isinstance(x, text_type):
+ return self._text_stream.write(x)
+ try:
+ self.flush()
+ except Exception:
+ pass
+ return self.buffer.write(x)
+
+ def writelines(self, lines):
+ for line in lines:
+ self.write(line)
+
+ def __getattr__(self, name):
+ return getattr(self._text_stream, name)
+
+ def isatty(self):
+ return self.buffer.isatty()
+
+ def __repr__(self):
+ return "".format(
+ self.name, self.encoding
+ )
+
+
+class WindowsChunkedWriter(object):
+ """
+ Wraps a stream (such as stdout), acting as a transparent proxy for all
+ attribute access apart from method 'write()' which we wrap to write in
+ limited chunks due to a Windows limitation on binary console streams.
+ """
+
+ def __init__(self, wrapped):
+ # double-underscore everything to prevent clashes with names of
+ # attributes on the wrapped stream object.
+ self.__wrapped = wrapped
+
+ def __getattr__(self, name):
+ return getattr(self.__wrapped, name)
+
+ def write(self, text):
+ total_to_write = len(text)
+ written = 0
+
+ while written < total_to_write:
+ to_write = min(total_to_write - written, MAX_BYTES_WRITTEN)
+ self.__wrapped.write(text[written : written + to_write])
+ written += to_write
+
+
+_wrapped_std_streams = set()
+
+
+def _wrap_std_stream(name):
+ # Python 2 & Windows 7 and below
+ if (
+ PY2
+ and sys.getwindowsversion()[:2] <= (6, 1)
+ and name not in _wrapped_std_streams
+ ):
+ setattr(sys, name, WindowsChunkedWriter(getattr(sys, name)))
+ _wrapped_std_streams.add(name)
+
+
+def _get_text_stdin(buffer_stream):
+ text_stream = _NonClosingTextIOWrapper(
+ io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
+ "utf-16-le",
+ "strict",
+ line_buffering=True,
+ )
+ return ConsoleStream(text_stream, buffer_stream)
+
+
+def _get_text_stdout(buffer_stream):
+ text_stream = _NonClosingTextIOWrapper(
+ io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
+ "utf-16-le",
+ "strict",
+ line_buffering=True,
+ )
+ return ConsoleStream(text_stream, buffer_stream)
+
+
+def _get_text_stderr(buffer_stream):
+ text_stream = _NonClosingTextIOWrapper(
+ io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
+ "utf-16-le",
+ "strict",
+ line_buffering=True,
+ )
+ return ConsoleStream(text_stream, buffer_stream)
+
+
+if PY2:
+
+ def _hash_py_argv():
+ return zlib.crc32("\x00".join(sys.argv[1:]))
+
+ _initial_argv_hash = _hash_py_argv()
+
+ def _get_windows_argv():
+ argc = c_int(0)
+ argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc))
+ if not argv_unicode:
+ raise WinError()
+ try:
+ argv = [argv_unicode[i] for i in range(0, argc.value)]
+ finally:
+ LocalFree(argv_unicode)
+ del argv_unicode
+
+ if not hasattr(sys, "frozen"):
+ argv = argv[1:]
+ while len(argv) > 0:
+ arg = argv[0]
+ if not arg.startswith("-") or arg == "-":
+ break
+ argv = argv[1:]
+ if arg.startswith(("-c", "-m")):
+ break
+
+ return argv[1:]
+
+
+_stream_factories = {
+ 0: _get_text_stdin,
+ 1: _get_text_stdout,
+ 2: _get_text_stderr,
+}
+
+
+def _is_console(f):
+ if not hasattr(f, "fileno"):
+ return False
+
+ try:
+ fileno = f.fileno()
+ except OSError:
+ return False
+
+ handle = msvcrt.get_osfhandle(fileno)
+ return bool(GetConsoleMode(handle, byref(DWORD())))
+
+
+def _get_windows_console_stream(f, encoding, errors):
+ if (
+ get_buffer is not None
+ and encoding in ("utf-16-le", None)
+ and errors in ("strict", None)
+ and _is_console(f)
+ ):
+ func = _stream_factories.get(f.fileno())
+ if func is not None:
+ if not PY2:
+ f = getattr(f, "buffer", None)
+ if f is None:
+ return None
+ else:
+ # If we are on Python 2 we need to set the stream that we
+ # deal with to binary mode as otherwise the exercise if a
+ # bit moot. The same problems apply as for
+ # get_binary_stdin and friends from _compat.
+ msvcrt.setmode(f.fileno(), os.O_BINARY)
+ return func(f)
diff --git a/mixly/tools/python/click/core.py b/mixly/tools/python/click/core.py
new file mode 100644
index 00000000..f58bf26d
--- /dev/null
+++ b/mixly/tools/python/click/core.py
@@ -0,0 +1,2030 @@
+import errno
+import inspect
+import os
+import sys
+from contextlib import contextmanager
+from functools import update_wrapper
+from itertools import repeat
+
+from ._compat import isidentifier
+from ._compat import iteritems
+from ._compat import PY2
+from ._compat import string_types
+from ._unicodefun import _check_for_unicode_literals
+from ._unicodefun import _verify_python3_env
+from .exceptions import Abort
+from .exceptions import BadParameter
+from .exceptions import ClickException
+from .exceptions import Exit
+from .exceptions import MissingParameter
+from .exceptions import UsageError
+from .formatting import HelpFormatter
+from .formatting import join_options
+from .globals import pop_context
+from .globals import push_context
+from .parser import OptionParser
+from .parser import split_opt
+from .termui import confirm
+from .termui import prompt
+from .termui import style
+from .types import BOOL
+from .types import convert_type
+from .types import IntRange
+from .utils import echo
+from .utils import get_os_args
+from .utils import make_default_short_help
+from .utils import make_str
+from .utils import PacifyFlushWrapper
+
+_missing = object()
+
+SUBCOMMAND_METAVAR = "COMMAND [ARGS]..."
+SUBCOMMANDS_METAVAR = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
+
+DEPRECATED_HELP_NOTICE = " (DEPRECATED)"
+DEPRECATED_INVOKE_NOTICE = "DeprecationWarning: The command %(name)s is deprecated."
+
+
+def _maybe_show_deprecated_notice(cmd):
+ if cmd.deprecated:
+ echo(style(DEPRECATED_INVOKE_NOTICE % {"name": cmd.name}, fg="red"), err=True)
+
+
+def fast_exit(code):
+ """Exit without garbage collection, this speeds up exit by about 10ms for
+ things like bash completion.
+ """
+ sys.stdout.flush()
+ sys.stderr.flush()
+ os._exit(code)
+
+
+def _bashcomplete(cmd, prog_name, complete_var=None):
+ """Internal handler for the bash completion support."""
+ if complete_var is None:
+ complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper())
+ complete_instr = os.environ.get(complete_var)
+ if not complete_instr:
+ return
+
+ from ._bashcomplete import bashcomplete
+
+ if bashcomplete(cmd, prog_name, complete_var, complete_instr):
+ fast_exit(1)
+
+
+def _check_multicommand(base_command, cmd_name, cmd, register=False):
+ if not base_command.chain or not isinstance(cmd, MultiCommand):
+ return
+ if register:
+ hint = (
+ "It is not possible to add multi commands as children to"
+ " another multi command that is in chain mode."
+ )
+ else:
+ hint = (
+ "Found a multi command as subcommand to a multi command"
+ " that is in chain mode. This is not supported."
+ )
+ raise RuntimeError(
+ "{}. Command '{}' is set to chain and '{}' was added as"
+ " subcommand but it in itself is a multi command. ('{}' is a {}"
+ " within a chained {} named '{}').".format(
+ hint,
+ base_command.name,
+ cmd_name,
+ cmd_name,
+ cmd.__class__.__name__,
+ base_command.__class__.__name__,
+ base_command.name,
+ )
+ )
+
+
+def batch(iterable, batch_size):
+ return list(zip(*repeat(iter(iterable), batch_size)))
+
+
+def invoke_param_callback(callback, ctx, param, value):
+ code = getattr(callback, "__code__", None)
+ args = getattr(code, "co_argcount", 3)
+
+ if args < 3:
+ from warnings import warn
+
+ warn(
+ "Parameter callbacks take 3 args, (ctx, param, value). The"
+ " 2-arg style is deprecated and will be removed in 8.0.".format(callback),
+ DeprecationWarning,
+ stacklevel=3,
+ )
+ return callback(ctx, value)
+
+ return callback(ctx, param, value)
+
+
+@contextmanager
+def augment_usage_errors(ctx, param=None):
+ """Context manager that attaches extra information to exceptions."""
+ try:
+ yield
+ except BadParameter as e:
+ if e.ctx is None:
+ e.ctx = ctx
+ if param is not None and e.param is None:
+ e.param = param
+ raise
+ except UsageError as e:
+ if e.ctx is None:
+ e.ctx = ctx
+ raise
+
+
+def iter_params_for_processing(invocation_order, declaration_order):
+ """Given a sequence of parameters in the order as should be considered
+ for processing and an iterable of parameters that exist, this returns
+ a list in the correct order as they should be processed.
+ """
+
+ def sort_key(item):
+ try:
+ idx = invocation_order.index(item)
+ except ValueError:
+ idx = float("inf")
+ return (not item.is_eager, idx)
+
+ return sorted(declaration_order, key=sort_key)
+
+
+class Context(object):
+ """The context is a special internal object that holds state relevant
+ for the script execution at every single level. It's normally invisible
+ to commands unless they opt-in to getting access to it.
+
+ The context is useful as it can pass internal objects around and can
+ control special execution features such as reading data from
+ environment variables.
+
+ A context can be used as context manager in which case it will call
+ :meth:`close` on teardown.
+
+ .. versionadded:: 2.0
+ Added the `resilient_parsing`, `help_option_names`,
+ `token_normalize_func` parameters.
+
+ .. versionadded:: 3.0
+ Added the `allow_extra_args` and `allow_interspersed_args`
+ parameters.
+
+ .. versionadded:: 4.0
+ Added the `color`, `ignore_unknown_options`, and
+ `max_content_width` parameters.
+
+ .. versionadded:: 7.1
+ Added the `show_default` parameter.
+
+ :param command: the command class for this context.
+ :param parent: the parent context.
+ :param info_name: the info name for this invocation. Generally this
+ is the most descriptive name for the script or
+ command. For the toplevel script it is usually
+ the name of the script, for commands below it it's
+ the name of the script.
+ :param obj: an arbitrary object of user data.
+ :param auto_envvar_prefix: the prefix to use for automatic environment
+ variables. If this is `None` then reading
+ from environment variables is disabled. This
+ does not affect manually set environment
+ variables which are always read.
+ :param default_map: a dictionary (like object) with default values
+ for parameters.
+ :param terminal_width: the width of the terminal. The default is
+ inherit from parent context. If no context
+ defines the terminal width then auto
+ detection will be applied.
+ :param max_content_width: the maximum width for content rendered by
+ Click (this currently only affects help
+ pages). This defaults to 80 characters if
+ not overridden. In other words: even if the
+ terminal is larger than that, Click will not
+ format things wider than 80 characters by
+ default. In addition to that, formatters might
+ add some safety mapping on the right.
+ :param resilient_parsing: if this flag is enabled then Click will
+ parse without any interactivity or callback
+ invocation. Default values will also be
+ ignored. This is useful for implementing
+ things such as completion support.
+ :param allow_extra_args: if this is set to `True` then extra arguments
+ at the end will not raise an error and will be
+ kept on the context. The default is to inherit
+ from the command.
+ :param allow_interspersed_args: if this is set to `False` then options
+ and arguments cannot be mixed. The
+ default is to inherit from the command.
+ :param ignore_unknown_options: instructs click to ignore options it does
+ not know and keeps them for later
+ processing.
+ :param help_option_names: optionally a list of strings that define how
+ the default help parameter is named. The
+ default is ``['--help']``.
+ :param token_normalize_func: an optional function that is used to
+ normalize tokens (options, choices,
+ etc.). This for instance can be used to
+ implement case insensitive behavior.
+ :param color: controls if the terminal supports ANSI colors or not. The
+ default is autodetection. This is only needed if ANSI
+ codes are used in texts that Click prints which is by
+ default not the case. This for instance would affect
+ help output.
+ :param show_default: if True, shows defaults for all options.
+ Even if an option is later created with show_default=False,
+ this command-level setting overrides it.
+ """
+
+ def __init__(
+ self,
+ command,
+ parent=None,
+ info_name=None,
+ obj=None,
+ auto_envvar_prefix=None,
+ default_map=None,
+ terminal_width=None,
+ max_content_width=None,
+ resilient_parsing=False,
+ allow_extra_args=None,
+ allow_interspersed_args=None,
+ ignore_unknown_options=None,
+ help_option_names=None,
+ token_normalize_func=None,
+ color=None,
+ show_default=None,
+ ):
+ #: the parent context or `None` if none exists.
+ self.parent = parent
+ #: the :class:`Command` for this context.
+ self.command = command
+ #: the descriptive information name
+ self.info_name = info_name
+ #: the parsed parameters except if the value is hidden in which
+ #: case it's not remembered.
+ self.params = {}
+ #: the leftover arguments.
+ self.args = []
+ #: protected arguments. These are arguments that are prepended
+ #: to `args` when certain parsing scenarios are encountered but
+ #: must be never propagated to another arguments. This is used
+ #: to implement nested parsing.
+ self.protected_args = []
+ if obj is None and parent is not None:
+ obj = parent.obj
+ #: the user object stored.
+ self.obj = obj
+ self._meta = getattr(parent, "meta", {})
+
+ #: A dictionary (-like object) with defaults for parameters.
+ if (
+ default_map is None
+ and parent is not None
+ and parent.default_map is not None
+ ):
+ default_map = parent.default_map.get(info_name)
+ self.default_map = default_map
+
+ #: This flag indicates if a subcommand is going to be executed. A
+ #: group callback can use this information to figure out if it's
+ #: being executed directly or because the execution flow passes
+ #: onwards to a subcommand. By default it's None, but it can be
+ #: the name of the subcommand to execute.
+ #:
+ #: If chaining is enabled this will be set to ``'*'`` in case
+ #: any commands are executed. It is however not possible to
+ #: figure out which ones. If you require this knowledge you
+ #: should use a :func:`resultcallback`.
+ self.invoked_subcommand = None
+
+ if terminal_width is None and parent is not None:
+ terminal_width = parent.terminal_width
+ #: The width of the terminal (None is autodetection).
+ self.terminal_width = terminal_width
+
+ if max_content_width is None and parent is not None:
+ max_content_width = parent.max_content_width
+ #: The maximum width of formatted content (None implies a sensible
+ #: default which is 80 for most things).
+ self.max_content_width = max_content_width
+
+ if allow_extra_args is None:
+ allow_extra_args = command.allow_extra_args
+ #: Indicates if the context allows extra args or if it should
+ #: fail on parsing.
+ #:
+ #: .. versionadded:: 3.0
+ self.allow_extra_args = allow_extra_args
+
+ if allow_interspersed_args is None:
+ allow_interspersed_args = command.allow_interspersed_args
+ #: Indicates if the context allows mixing of arguments and
+ #: options or not.
+ #:
+ #: .. versionadded:: 3.0
+ self.allow_interspersed_args = allow_interspersed_args
+
+ if ignore_unknown_options is None:
+ ignore_unknown_options = command.ignore_unknown_options
+ #: Instructs click to ignore options that a command does not
+ #: understand and will store it on the context for later
+ #: processing. This is primarily useful for situations where you
+ #: want to call into external programs. Generally this pattern is
+ #: strongly discouraged because it's not possibly to losslessly
+ #: forward all arguments.
+ #:
+ #: .. versionadded:: 4.0
+ self.ignore_unknown_options = ignore_unknown_options
+
+ if help_option_names is None:
+ if parent is not None:
+ help_option_names = parent.help_option_names
+ else:
+ help_option_names = ["--help"]
+
+ #: The names for the help options.
+ self.help_option_names = help_option_names
+
+ if token_normalize_func is None and parent is not None:
+ token_normalize_func = parent.token_normalize_func
+
+ #: An optional normalization function for tokens. This is
+ #: options, choices, commands etc.
+ self.token_normalize_func = token_normalize_func
+
+ #: Indicates if resilient parsing is enabled. In that case Click
+ #: will do its best to not cause any failures and default values
+ #: will be ignored. Useful for completion.
+ self.resilient_parsing = resilient_parsing
+
+ # If there is no envvar prefix yet, but the parent has one and
+ # the command on this level has a name, we can expand the envvar
+ # prefix automatically.
+ if auto_envvar_prefix is None:
+ if (
+ parent is not None
+ and parent.auto_envvar_prefix is not None
+ and self.info_name is not None
+ ):
+ auto_envvar_prefix = "{}_{}".format(
+ parent.auto_envvar_prefix, self.info_name.upper()
+ )
+ else:
+ auto_envvar_prefix = auto_envvar_prefix.upper()
+ if auto_envvar_prefix is not None:
+ auto_envvar_prefix = auto_envvar_prefix.replace("-", "_")
+ self.auto_envvar_prefix = auto_envvar_prefix
+
+ if color is None and parent is not None:
+ color = parent.color
+
+ #: Controls if styling output is wanted or not.
+ self.color = color
+
+ self.show_default = show_default
+
+ self._close_callbacks = []
+ self._depth = 0
+
+ def __enter__(self):
+ self._depth += 1
+ push_context(self)
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb):
+ self._depth -= 1
+ if self._depth == 0:
+ self.close()
+ pop_context()
+
+ @contextmanager
+ def scope(self, cleanup=True):
+ """This helper method can be used with the context object to promote
+ it to the current thread local (see :func:`get_current_context`).
+ The default behavior of this is to invoke the cleanup functions which
+ can be disabled by setting `cleanup` to `False`. The cleanup
+ functions are typically used for things such as closing file handles.
+
+ If the cleanup is intended the context object can also be directly
+ used as a context manager.
+
+ Example usage::
+
+ with ctx.scope():
+ assert get_current_context() is ctx
+
+ This is equivalent::
+
+ with ctx:
+ assert get_current_context() is ctx
+
+ .. versionadded:: 5.0
+
+ :param cleanup: controls if the cleanup functions should be run or
+ not. The default is to run these functions. In
+ some situations the context only wants to be
+ temporarily pushed in which case this can be disabled.
+ Nested pushes automatically defer the cleanup.
+ """
+ if not cleanup:
+ self._depth += 1
+ try:
+ with self as rv:
+ yield rv
+ finally:
+ if not cleanup:
+ self._depth -= 1
+
+ @property
+ def meta(self):
+ """This is a dictionary which is shared with all the contexts
+ that are nested. It exists so that click utilities can store some
+ state here if they need to. It is however the responsibility of
+ that code to manage this dictionary well.
+
+ The keys are supposed to be unique dotted strings. For instance
+ module paths are a good choice for it. What is stored in there is
+ irrelevant for the operation of click. However what is important is
+ that code that places data here adheres to the general semantics of
+ the system.
+
+ Example usage::
+
+ LANG_KEY = f'{__name__}.lang'
+
+ def set_language(value):
+ ctx = get_current_context()
+ ctx.meta[LANG_KEY] = value
+
+ def get_language():
+ return get_current_context().meta.get(LANG_KEY, 'en_US')
+
+ .. versionadded:: 5.0
+ """
+ return self._meta
+
+ def make_formatter(self):
+ """Creates the formatter for the help and usage output."""
+ return HelpFormatter(
+ width=self.terminal_width, max_width=self.max_content_width
+ )
+
+ def call_on_close(self, f):
+ """This decorator remembers a function as callback that should be
+ executed when the context tears down. This is most useful to bind
+ resource handling to the script execution. For instance, file objects
+ opened by the :class:`File` type will register their close callbacks
+ here.
+
+ :param f: the function to execute on teardown.
+ """
+ self._close_callbacks.append(f)
+ return f
+
+ def close(self):
+ """Invokes all close callbacks."""
+ for cb in self._close_callbacks:
+ cb()
+ self._close_callbacks = []
+
+ @property
+ def command_path(self):
+ """The computed command path. This is used for the ``usage``
+ information on the help page. It's automatically created by
+ combining the info names of the chain of contexts to the root.
+ """
+ rv = ""
+ if self.info_name is not None:
+ rv = self.info_name
+ if self.parent is not None:
+ rv = "{} {}".format(self.parent.command_path, rv)
+ return rv.lstrip()
+
+ def find_root(self):
+ """Finds the outermost context."""
+ node = self
+ while node.parent is not None:
+ node = node.parent
+ return node
+
+ def find_object(self, object_type):
+ """Finds the closest object of a given type."""
+ node = self
+ while node is not None:
+ if isinstance(node.obj, object_type):
+ return node.obj
+ node = node.parent
+
+ def ensure_object(self, object_type):
+ """Like :meth:`find_object` but sets the innermost object to a
+ new instance of `object_type` if it does not exist.
+ """
+ rv = self.find_object(object_type)
+ if rv is None:
+ self.obj = rv = object_type()
+ return rv
+
+ def lookup_default(self, name):
+ """Looks up the default for a parameter name. This by default
+ looks into the :attr:`default_map` if available.
+ """
+ if self.default_map is not None:
+ rv = self.default_map.get(name)
+ if callable(rv):
+ rv = rv()
+ return rv
+
+ def fail(self, message):
+ """Aborts the execution of the program with a specific error
+ message.
+
+ :param message: the error message to fail with.
+ """
+ raise UsageError(message, self)
+
+ def abort(self):
+ """Aborts the script."""
+ raise Abort()
+
+ def exit(self, code=0):
+ """Exits the application with a given exit code."""
+ raise Exit(code)
+
+ def get_usage(self):
+ """Helper method to get formatted usage string for the current
+ context and command.
+ """
+ return self.command.get_usage(self)
+
+ def get_help(self):
+ """Helper method to get formatted help page for the current
+ context and command.
+ """
+ return self.command.get_help(self)
+
+ def invoke(*args, **kwargs): # noqa: B902
+ """Invokes a command callback in exactly the way it expects. There
+ are two ways to invoke this method:
+
+ 1. the first argument can be a callback and all other arguments and
+ keyword arguments are forwarded directly to the function.
+ 2. the first argument is a click command object. In that case all
+ arguments are forwarded as well but proper click parameters
+ (options and click arguments) must be keyword arguments and Click
+ will fill in defaults.
+
+ Note that before Click 3.2 keyword arguments were not properly filled
+ in against the intention of this code and no context was created. For
+ more information about this change and why it was done in a bugfix
+ release see :ref:`upgrade-to-3.2`.
+ """
+ self, callback = args[:2]
+ ctx = self
+
+ # It's also possible to invoke another command which might or
+ # might not have a callback. In that case we also fill
+ # in defaults and make a new context for this command.
+ if isinstance(callback, Command):
+ other_cmd = callback
+ callback = other_cmd.callback
+ ctx = Context(other_cmd, info_name=other_cmd.name, parent=self)
+ if callback is None:
+ raise TypeError(
+ "The given command does not have a callback that can be invoked."
+ )
+
+ for param in other_cmd.params:
+ if param.name not in kwargs and param.expose_value:
+ kwargs[param.name] = param.get_default(ctx)
+
+ args = args[2:]
+ with augment_usage_errors(self):
+ with ctx:
+ return callback(*args, **kwargs)
+
+ def forward(*args, **kwargs): # noqa: B902
+ """Similar to :meth:`invoke` but fills in default keyword
+ arguments from the current context if the other command expects
+ it. This cannot invoke callbacks directly, only other commands.
+ """
+ self, cmd = args[:2]
+
+ # It's also possible to invoke another command which might or
+ # might not have a callback.
+ if not isinstance(cmd, Command):
+ raise TypeError("Callback is not a command.")
+
+ for param in self.params:
+ if param not in kwargs:
+ kwargs[param] = self.params[param]
+
+ return self.invoke(cmd, **kwargs)
+
+
+class BaseCommand(object):
+ """The base command implements the minimal API contract of commands.
+ Most code will never use this as it does not implement a lot of useful
+ functionality but it can act as the direct subclass of alternative
+ parsing methods that do not depend on the Click parser.
+
+ For instance, this can be used to bridge Click and other systems like
+ argparse or docopt.
+
+ Because base commands do not implement a lot of the API that other
+ parts of Click take for granted, they are not supported for all
+ operations. For instance, they cannot be used with the decorators
+ usually and they have no built-in callback system.
+
+ .. versionchanged:: 2.0
+ Added the `context_settings` parameter.
+
+ :param name: the name of the command to use unless a group overrides it.
+ :param context_settings: an optional dictionary with defaults that are
+ passed to the context object.
+ """
+
+ #: the default for the :attr:`Context.allow_extra_args` flag.
+ allow_extra_args = False
+ #: the default for the :attr:`Context.allow_interspersed_args` flag.
+ allow_interspersed_args = True
+ #: the default for the :attr:`Context.ignore_unknown_options` flag.
+ ignore_unknown_options = False
+
+ def __init__(self, name, context_settings=None):
+ #: the name the command thinks it has. Upon registering a command
+ #: on a :class:`Group` the group will default the command name
+ #: with this information. You should instead use the
+ #: :class:`Context`\'s :attr:`~Context.info_name` attribute.
+ self.name = name
+ if context_settings is None:
+ context_settings = {}
+ #: an optional dictionary with defaults passed to the context.
+ self.context_settings = context_settings
+
+ def __repr__(self):
+ return "<{} {}>".format(self.__class__.__name__, self.name)
+
+ def get_usage(self, ctx):
+ raise NotImplementedError("Base commands cannot get usage")
+
+ def get_help(self, ctx):
+ raise NotImplementedError("Base commands cannot get help")
+
+ def make_context(self, info_name, args, parent=None, **extra):
+ """This function when given an info name and arguments will kick
+ off the parsing and create a new :class:`Context`. It does not
+ invoke the actual command callback though.
+
+ :param info_name: the info name for this invokation. Generally this
+ is the most descriptive name for the script or
+ command. For the toplevel script it's usually
+ the name of the script, for commands below it it's
+ the name of the script.
+ :param args: the arguments to parse as list of strings.
+ :param parent: the parent context if available.
+ :param extra: extra keyword arguments forwarded to the context
+ constructor.
+ """
+ for key, value in iteritems(self.context_settings):
+ if key not in extra:
+ extra[key] = value
+ ctx = Context(self, info_name=info_name, parent=parent, **extra)
+ with ctx.scope(cleanup=False):
+ self.parse_args(ctx, args)
+ return ctx
+
+ def parse_args(self, ctx, args):
+ """Given a context and a list of arguments this creates the parser
+ and parses the arguments, then modifies the context as necessary.
+ This is automatically invoked by :meth:`make_context`.
+ """
+ raise NotImplementedError("Base commands do not know how to parse arguments.")
+
+ def invoke(self, ctx):
+ """Given a context, this invokes the command. The default
+ implementation is raising a not implemented error.
+ """
+ raise NotImplementedError("Base commands are not invokable by default")
+
+ def main(
+ self,
+ args=None,
+ prog_name=None,
+ complete_var=None,
+ standalone_mode=True,
+ **extra
+ ):
+ """This is the way to invoke a script with all the bells and
+ whistles as a command line application. This will always terminate
+ the application after a call. If this is not wanted, ``SystemExit``
+ needs to be caught.
+
+ This method is also available by directly calling the instance of
+ a :class:`Command`.
+
+ .. versionadded:: 3.0
+ Added the `standalone_mode` flag to control the standalone mode.
+
+ :param args: the arguments that should be used for parsing. If not
+ provided, ``sys.argv[1:]`` is used.
+ :param prog_name: the program name that should be used. By default
+ the program name is constructed by taking the file
+ name from ``sys.argv[0]``.
+ :param complete_var: the environment variable that controls the
+ bash completion support. The default is
+ ``"__COMPLETE"`` with prog_name in
+ uppercase.
+ :param standalone_mode: the default behavior is to invoke the script
+ in standalone mode. Click will then
+ handle exceptions and convert them into
+ error messages and the function will never
+ return but shut down the interpreter. If
+ this is set to `False` they will be
+ propagated to the caller and the return
+ value of this function is the return value
+ of :meth:`invoke`.
+ :param extra: extra keyword arguments are forwarded to the context
+ constructor. See :class:`Context` for more information.
+ """
+ # If we are in Python 3, we will verify that the environment is
+ # sane at this point or reject further execution to avoid a
+ # broken script.
+ if not PY2:
+ _verify_python3_env()
+ else:
+ _check_for_unicode_literals()
+
+ if args is None:
+ args = get_os_args()
+ else:
+ args = list(args)
+
+ if prog_name is None:
+ prog_name = make_str(
+ os.path.basename(sys.argv[0] if sys.argv else __file__)
+ )
+
+ # Hook for the Bash completion. This only activates if the Bash
+ # completion is actually enabled, otherwise this is quite a fast
+ # noop.
+ _bashcomplete(self, prog_name, complete_var)
+
+ try:
+ try:
+ with self.make_context(prog_name, args, **extra) as ctx:
+ rv = self.invoke(ctx)
+ if not standalone_mode:
+ return rv
+ # it's not safe to `ctx.exit(rv)` here!
+ # note that `rv` may actually contain data like "1" which
+ # has obvious effects
+ # more subtle case: `rv=[None, None]` can come out of
+ # chained commands which all returned `None` -- so it's not
+ # even always obvious that `rv` indicates success/failure
+ # by its truthiness/falsiness
+ ctx.exit()
+ except (EOFError, KeyboardInterrupt):
+ echo(file=sys.stderr)
+ raise Abort()
+ except ClickException as e:
+ if not standalone_mode:
+ raise
+ e.show()
+ sys.exit(e.exit_code)
+ except IOError as e:
+ if e.errno == errno.EPIPE:
+ sys.stdout = PacifyFlushWrapper(sys.stdout)
+ sys.stderr = PacifyFlushWrapper(sys.stderr)
+ sys.exit(1)
+ else:
+ raise
+ except Exit as e:
+ if standalone_mode:
+ sys.exit(e.exit_code)
+ else:
+ # in non-standalone mode, return the exit code
+ # note that this is only reached if `self.invoke` above raises
+ # an Exit explicitly -- thus bypassing the check there which
+ # would return its result
+ # the results of non-standalone execution may therefore be
+ # somewhat ambiguous: if there are codepaths which lead to
+ # `ctx.exit(1)` and to `return 1`, the caller won't be able to
+ # tell the difference between the two
+ return e.exit_code
+ except Abort:
+ if not standalone_mode:
+ raise
+ echo("Aborted!", file=sys.stderr)
+ sys.exit(1)
+
+ def __call__(self, *args, **kwargs):
+ """Alias for :meth:`main`."""
+ return self.main(*args, **kwargs)
+
+
+class Command(BaseCommand):
+ """Commands are the basic building block of command line interfaces in
+ Click. A basic command handles command line parsing and might dispatch
+ more parsing to commands nested below it.
+
+ .. versionchanged:: 2.0
+ Added the `context_settings` parameter.
+ .. versionchanged:: 7.1
+ Added the `no_args_is_help` parameter.
+
+ :param name: the name of the command to use unless a group overrides it.
+ :param context_settings: an optional dictionary with defaults that are
+ passed to the context object.
+ :param callback: the callback to invoke. This is optional.
+ :param params: the parameters to register with this command. This can
+ be either :class:`Option` or :class:`Argument` objects.
+ :param help: the help string to use for this command.
+ :param epilog: like the help string but it's printed at the end of the
+ help page after everything else.
+ :param short_help: the short help to use for this command. This is
+ shown on the command listing of the parent command.
+ :param add_help_option: by default each command registers a ``--help``
+ option. This can be disabled by this parameter.
+ :param no_args_is_help: this controls what happens if no arguments are
+ provided. This option is disabled by default.
+ If enabled this will add ``--help`` as argument
+ if no arguments are passed
+ :param hidden: hide this command from help outputs.
+
+ :param deprecated: issues a message indicating that
+ the command is deprecated.
+ """
+
+ def __init__(
+ self,
+ name,
+ context_settings=None,
+ callback=None,
+ params=None,
+ help=None,
+ epilog=None,
+ short_help=None,
+ options_metavar="[OPTIONS]",
+ add_help_option=True,
+ no_args_is_help=False,
+ hidden=False,
+ deprecated=False,
+ ):
+ BaseCommand.__init__(self, name, context_settings)
+ #: the callback to execute when the command fires. This might be
+ #: `None` in which case nothing happens.
+ self.callback = callback
+ #: the list of parameters for this command in the order they
+ #: should show up in the help page and execute. Eager parameters
+ #: will automatically be handled before non eager ones.
+ self.params = params or []
+ # if a form feed (page break) is found in the help text, truncate help
+ # text to the content preceding the first form feed
+ if help and "\f" in help:
+ help = help.split("\f", 1)[0]
+ self.help = help
+ self.epilog = epilog
+ self.options_metavar = options_metavar
+ self.short_help = short_help
+ self.add_help_option = add_help_option
+ self.no_args_is_help = no_args_is_help
+ self.hidden = hidden
+ self.deprecated = deprecated
+
+ def get_usage(self, ctx):
+ """Formats the usage line into a string and returns it.
+
+ Calls :meth:`format_usage` internally.
+ """
+ formatter = ctx.make_formatter()
+ self.format_usage(ctx, formatter)
+ return formatter.getvalue().rstrip("\n")
+
+ def get_params(self, ctx):
+ rv = self.params
+ help_option = self.get_help_option(ctx)
+ if help_option is not None:
+ rv = rv + [help_option]
+ return rv
+
+ def format_usage(self, ctx, formatter):
+ """Writes the usage line into the formatter.
+
+ This is a low-level method called by :meth:`get_usage`.
+ """
+ pieces = self.collect_usage_pieces(ctx)
+ formatter.write_usage(ctx.command_path, " ".join(pieces))
+
+ def collect_usage_pieces(self, ctx):
+ """Returns all the pieces that go into the usage line and returns
+ it as a list of strings.
+ """
+ rv = [self.options_metavar]
+ for param in self.get_params(ctx):
+ rv.extend(param.get_usage_pieces(ctx))
+ return rv
+
+ def get_help_option_names(self, ctx):
+ """Returns the names for the help option."""
+ all_names = set(ctx.help_option_names)
+ for param in self.params:
+ all_names.difference_update(param.opts)
+ all_names.difference_update(param.secondary_opts)
+ return all_names
+
+ def get_help_option(self, ctx):
+ """Returns the help option object."""
+ help_options = self.get_help_option_names(ctx)
+ if not help_options or not self.add_help_option:
+ return
+
+ def show_help(ctx, param, value):
+ if value and not ctx.resilient_parsing:
+ echo(ctx.get_help(), color=ctx.color)
+ ctx.exit()
+
+ return Option(
+ help_options,
+ is_flag=True,
+ is_eager=True,
+ expose_value=False,
+ callback=show_help,
+ help="Show this message and exit.",
+ )
+
+ def make_parser(self, ctx):
+ """Creates the underlying option parser for this command."""
+ parser = OptionParser(ctx)
+ for param in self.get_params(ctx):
+ param.add_to_parser(parser, ctx)
+ return parser
+
+ def get_help(self, ctx):
+ """Formats the help into a string and returns it.
+
+ Calls :meth:`format_help` internally.
+ """
+ formatter = ctx.make_formatter()
+ self.format_help(ctx, formatter)
+ return formatter.getvalue().rstrip("\n")
+
+ def get_short_help_str(self, limit=45):
+ """Gets short help for the command or makes it by shortening the
+ long help string.
+ """
+ return (
+ self.short_help
+ or self.help
+ and make_default_short_help(self.help, limit)
+ or ""
+ )
+
+ def format_help(self, ctx, formatter):
+ """Writes the help into the formatter if it exists.
+
+ This is a low-level method called by :meth:`get_help`.
+
+ This calls the following methods:
+
+ - :meth:`format_usage`
+ - :meth:`format_help_text`
+ - :meth:`format_options`
+ - :meth:`format_epilog`
+ """
+ self.format_usage(ctx, formatter)
+ self.format_help_text(ctx, formatter)
+ self.format_options(ctx, formatter)
+ self.format_epilog(ctx, formatter)
+
+ def format_help_text(self, ctx, formatter):
+ """Writes the help text to the formatter if it exists."""
+ if self.help:
+ formatter.write_paragraph()
+ with formatter.indentation():
+ help_text = self.help
+ if self.deprecated:
+ help_text += DEPRECATED_HELP_NOTICE
+ formatter.write_text(help_text)
+ elif self.deprecated:
+ formatter.write_paragraph()
+ with formatter.indentation():
+ formatter.write_text(DEPRECATED_HELP_NOTICE)
+
+ def format_options(self, ctx, formatter):
+ """Writes all the options into the formatter if they exist."""
+ opts = []
+ for param in self.get_params(ctx):
+ rv = param.get_help_record(ctx)
+ if rv is not None:
+ opts.append(rv)
+
+ if opts:
+ with formatter.section("Options"):
+ formatter.write_dl(opts)
+
+ def format_epilog(self, ctx, formatter):
+ """Writes the epilog into the formatter if it exists."""
+ if self.epilog:
+ formatter.write_paragraph()
+ with formatter.indentation():
+ formatter.write_text(self.epilog)
+
+ def parse_args(self, ctx, args):
+ if not args and self.no_args_is_help and not ctx.resilient_parsing:
+ echo(ctx.get_help(), color=ctx.color)
+ ctx.exit()
+
+ parser = self.make_parser(ctx)
+ opts, args, param_order = parser.parse_args(args=args)
+
+ for param in iter_params_for_processing(param_order, self.get_params(ctx)):
+ value, args = param.handle_parse_result(ctx, opts, args)
+
+ if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
+ ctx.fail(
+ "Got unexpected extra argument{} ({})".format(
+ "s" if len(args) != 1 else "", " ".join(map(make_str, args))
+ )
+ )
+
+ ctx.args = args
+ return args
+
+ def invoke(self, ctx):
+ """Given a context, this invokes the attached callback (if it exists)
+ in the right way.
+ """
+ _maybe_show_deprecated_notice(self)
+ if self.callback is not None:
+ return ctx.invoke(self.callback, **ctx.params)
+
+
+class MultiCommand(Command):
+ """A multi command is the basic implementation of a command that
+ dispatches to subcommands. The most common version is the
+ :class:`Group`.
+
+ :param invoke_without_command: this controls how the multi command itself
+ is invoked. By default it's only invoked
+ if a subcommand is provided.
+ :param no_args_is_help: this controls what happens if no arguments are
+ provided. This option is enabled by default if
+ `invoke_without_command` is disabled or disabled
+ if it's enabled. If enabled this will add
+ ``--help`` as argument if no arguments are
+ passed.
+ :param subcommand_metavar: the string that is used in the documentation
+ to indicate the subcommand place.
+ :param chain: if this is set to `True` chaining of multiple subcommands
+ is enabled. This restricts the form of commands in that
+ they cannot have optional arguments but it allows
+ multiple commands to be chained together.
+ :param result_callback: the result callback to attach to this multi
+ command.
+ """
+
+ allow_extra_args = True
+ allow_interspersed_args = False
+
+ def __init__(
+ self,
+ name=None,
+ invoke_without_command=False,
+ no_args_is_help=None,
+ subcommand_metavar=None,
+ chain=False,
+ result_callback=None,
+ **attrs
+ ):
+ Command.__init__(self, name, **attrs)
+ if no_args_is_help is None:
+ no_args_is_help = not invoke_without_command
+ self.no_args_is_help = no_args_is_help
+ self.invoke_without_command = invoke_without_command
+ if subcommand_metavar is None:
+ if chain:
+ subcommand_metavar = SUBCOMMANDS_METAVAR
+ else:
+ subcommand_metavar = SUBCOMMAND_METAVAR
+ self.subcommand_metavar = subcommand_metavar
+ self.chain = chain
+ #: The result callback that is stored. This can be set or
+ #: overridden with the :func:`resultcallback` decorator.
+ self.result_callback = result_callback
+
+ if self.chain:
+ for param in self.params:
+ if isinstance(param, Argument) and not param.required:
+ raise RuntimeError(
+ "Multi commands in chain mode cannot have"
+ " optional arguments."
+ )
+
+ def collect_usage_pieces(self, ctx):
+ rv = Command.collect_usage_pieces(self, ctx)
+ rv.append(self.subcommand_metavar)
+ return rv
+
+ def format_options(self, ctx, formatter):
+ Command.format_options(self, ctx, formatter)
+ self.format_commands(ctx, formatter)
+
+ def resultcallback(self, replace=False):
+ """Adds a result callback to the chain command. By default if a
+ result callback is already registered this will chain them but
+ this can be disabled with the `replace` parameter. The result
+ callback is invoked with the return value of the subcommand
+ (or the list of return values from all subcommands if chaining
+ is enabled) as well as the parameters as they would be passed
+ to the main callback.
+
+ Example::
+
+ @click.group()
+ @click.option('-i', '--input', default=23)
+ def cli(input):
+ return 42
+
+ @cli.resultcallback()
+ def process_result(result, input):
+ return result + input
+
+ .. versionadded:: 3.0
+
+ :param replace: if set to `True` an already existing result
+ callback will be removed.
+ """
+
+ def decorator(f):
+ old_callback = self.result_callback
+ if old_callback is None or replace:
+ self.result_callback = f
+ return f
+
+ def function(__value, *args, **kwargs):
+ return f(old_callback(__value, *args, **kwargs), *args, **kwargs)
+
+ self.result_callback = rv = update_wrapper(function, f)
+ return rv
+
+ return decorator
+
+ def format_commands(self, ctx, formatter):
+ """Extra format methods for multi methods that adds all the commands
+ after the options.
+ """
+ commands = []
+ for subcommand in self.list_commands(ctx):
+ cmd = self.get_command(ctx, subcommand)
+ # What is this, the tool lied about a command. Ignore it
+ if cmd is None:
+ continue
+ if cmd.hidden:
+ continue
+
+ commands.append((subcommand, cmd))
+
+ # allow for 3 times the default spacing
+ if len(commands):
+ limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)
+
+ rows = []
+ for subcommand, cmd in commands:
+ help = cmd.get_short_help_str(limit)
+ rows.append((subcommand, help))
+
+ if rows:
+ with formatter.section("Commands"):
+ formatter.write_dl(rows)
+
+ def parse_args(self, ctx, args):
+ if not args and self.no_args_is_help and not ctx.resilient_parsing:
+ echo(ctx.get_help(), color=ctx.color)
+ ctx.exit()
+
+ rest = Command.parse_args(self, ctx, args)
+ if self.chain:
+ ctx.protected_args = rest
+ ctx.args = []
+ elif rest:
+ ctx.protected_args, ctx.args = rest[:1], rest[1:]
+
+ return ctx.args
+
+ def invoke(self, ctx):
+ def _process_result(value):
+ if self.result_callback is not None:
+ value = ctx.invoke(self.result_callback, value, **ctx.params)
+ return value
+
+ if not ctx.protected_args:
+ # If we are invoked without command the chain flag controls
+ # how this happens. If we are not in chain mode, the return
+ # value here is the return value of the command.
+ # If however we are in chain mode, the return value is the
+ # return value of the result processor invoked with an empty
+ # list (which means that no subcommand actually was executed).
+ if self.invoke_without_command:
+ if not self.chain:
+ return Command.invoke(self, ctx)
+ with ctx:
+ Command.invoke(self, ctx)
+ return _process_result([])
+ ctx.fail("Missing command.")
+
+ # Fetch args back out
+ args = ctx.protected_args + ctx.args
+ ctx.args = []
+ ctx.protected_args = []
+
+ # If we're not in chain mode, we only allow the invocation of a
+ # single command but we also inform the current context about the
+ # name of the command to invoke.
+ if not self.chain:
+ # Make sure the context is entered so we do not clean up
+ # resources until the result processor has worked.
+ with ctx:
+ cmd_name, cmd, args = self.resolve_command(ctx, args)
+ ctx.invoked_subcommand = cmd_name
+ Command.invoke(self, ctx)
+ sub_ctx = cmd.make_context(cmd_name, args, parent=ctx)
+ with sub_ctx:
+ return _process_result(sub_ctx.command.invoke(sub_ctx))
+
+ # In chain mode we create the contexts step by step, but after the
+ # base command has been invoked. Because at that point we do not
+ # know the subcommands yet, the invoked subcommand attribute is
+ # set to ``*`` to inform the command that subcommands are executed
+ # but nothing else.
+ with ctx:
+ ctx.invoked_subcommand = "*" if args else None
+ Command.invoke(self, ctx)
+
+ # Otherwise we make every single context and invoke them in a
+ # chain. In that case the return value to the result processor
+ # is the list of all invoked subcommand's results.
+ contexts = []
+ while args:
+ cmd_name, cmd, args = self.resolve_command(ctx, args)
+ sub_ctx = cmd.make_context(
+ cmd_name,
+ args,
+ parent=ctx,
+ allow_extra_args=True,
+ allow_interspersed_args=False,
+ )
+ contexts.append(sub_ctx)
+ args, sub_ctx.args = sub_ctx.args, []
+
+ rv = []
+ for sub_ctx in contexts:
+ with sub_ctx:
+ rv.append(sub_ctx.command.invoke(sub_ctx))
+ return _process_result(rv)
+
+ def resolve_command(self, ctx, args):
+ cmd_name = make_str(args[0])
+ original_cmd_name = cmd_name
+
+ # Get the command
+ cmd = self.get_command(ctx, cmd_name)
+
+ # If we can't find the command but there is a normalization
+ # function available, we try with that one.
+ if cmd is None and ctx.token_normalize_func is not None:
+ cmd_name = ctx.token_normalize_func(cmd_name)
+ cmd = self.get_command(ctx, cmd_name)
+
+ # If we don't find the command we want to show an error message
+ # to the user that it was not provided. However, there is
+ # something else we should do: if the first argument looks like
+ # an option we want to kick off parsing again for arguments to
+ # resolve things like --help which now should go to the main
+ # place.
+ if cmd is None and not ctx.resilient_parsing:
+ if split_opt(cmd_name)[0]:
+ self.parse_args(ctx, ctx.args)
+ ctx.fail("No such command '{}'.".format(original_cmd_name))
+
+ return cmd_name, cmd, args[1:]
+
+ def get_command(self, ctx, cmd_name):
+ """Given a context and a command name, this returns a
+ :class:`Command` object if it exists or returns `None`.
+ """
+ raise NotImplementedError()
+
+ def list_commands(self, ctx):
+ """Returns a list of subcommand names in the order they should
+ appear.
+ """
+ return []
+
+
+class Group(MultiCommand):
+ """A group allows a command to have subcommands attached. This is the
+ most common way to implement nesting in Click.
+
+ :param commands: a dictionary of commands.
+ """
+
+ def __init__(self, name=None, commands=None, **attrs):
+ MultiCommand.__init__(self, name, **attrs)
+ #: the registered subcommands by their exported names.
+ self.commands = commands or {}
+
+ def add_command(self, cmd, name=None):
+ """Registers another :class:`Command` with this group. If the name
+ is not provided, the name of the command is used.
+ """
+ name = name or cmd.name
+ if name is None:
+ raise TypeError("Command has no name.")
+ _check_multicommand(self, name, cmd, register=True)
+ self.commands[name] = cmd
+
+ def command(self, *args, **kwargs):
+ """A shortcut decorator for declaring and attaching a command to
+ the group. This takes the same arguments as :func:`command` but
+ immediately registers the created command with this instance by
+ calling into :meth:`add_command`.
+ """
+ from .decorators import command
+
+ def decorator(f):
+ cmd = command(*args, **kwargs)(f)
+ self.add_command(cmd)
+ return cmd
+
+ return decorator
+
+ def group(self, *args, **kwargs):
+ """A shortcut decorator for declaring and attaching a group to
+ the group. This takes the same arguments as :func:`group` but
+ immediately registers the created command with this instance by
+ calling into :meth:`add_command`.
+ """
+ from .decorators import group
+
+ def decorator(f):
+ cmd = group(*args, **kwargs)(f)
+ self.add_command(cmd)
+ return cmd
+
+ return decorator
+
+ def get_command(self, ctx, cmd_name):
+ return self.commands.get(cmd_name)
+
+ def list_commands(self, ctx):
+ return sorted(self.commands)
+
+
+class CommandCollection(MultiCommand):
+ """A command collection is a multi command that merges multiple multi
+ commands together into one. This is a straightforward implementation
+ that accepts a list of different multi commands as sources and
+ provides all the commands for each of them.
+ """
+
+ def __init__(self, name=None, sources=None, **attrs):
+ MultiCommand.__init__(self, name, **attrs)
+ #: The list of registered multi commands.
+ self.sources = sources or []
+
+ def add_source(self, multi_cmd):
+ """Adds a new multi command to the chain dispatcher."""
+ self.sources.append(multi_cmd)
+
+ def get_command(self, ctx, cmd_name):
+ for source in self.sources:
+ rv = source.get_command(ctx, cmd_name)
+ if rv is not None:
+ if self.chain:
+ _check_multicommand(self, cmd_name, rv)
+ return rv
+
+ def list_commands(self, ctx):
+ rv = set()
+ for source in self.sources:
+ rv.update(source.list_commands(ctx))
+ return sorted(rv)
+
+
+class Parameter(object):
+ r"""A parameter to a command comes in two versions: they are either
+ :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently
+ not supported by design as some of the internals for parsing are
+ intentionally not finalized.
+
+ Some settings are supported by both options and arguments.
+
+ :param param_decls: the parameter declarations for this option or
+ argument. This is a list of flags or argument
+ names.
+ :param type: the type that should be used. Either a :class:`ParamType`
+ or a Python type. The later is converted into the former
+ automatically if supported.
+ :param required: controls if this is optional or not.
+ :param default: the default value if omitted. This can also be a callable,
+ in which case it's invoked when the default is needed
+ without any arguments.
+ :param callback: a callback that should be executed after the parameter
+ was matched. This is called as ``fn(ctx, param,
+ value)`` and needs to return the value.
+ :param nargs: the number of arguments to match. If not ``1`` the return
+ value is a tuple instead of single value. The default for
+ nargs is ``1`` (except if the type is a tuple, then it's
+ the arity of the tuple).
+ :param metavar: how the value is represented in the help page.
+ :param expose_value: if this is `True` then the value is passed onwards
+ to the command callback and stored on the context,
+ otherwise it's skipped.
+ :param is_eager: eager values are processed before non eager ones. This
+ should not be set for arguments or it will inverse the
+ order of processing.
+ :param envvar: a string or list of strings that are environment variables
+ that should be checked.
+
+ .. versionchanged:: 7.1
+ Empty environment variables are ignored rather than taking the
+ empty string value. This makes it possible for scripts to clear
+ variables if they can't unset them.
+
+ .. versionchanged:: 2.0
+ Changed signature for parameter callback to also be passed the
+ parameter. The old callback format will still work, but it will
+ raise a warning to give you a chance to migrate the code easier.
+ """
+ param_type_name = "parameter"
+
+ def __init__(
+ self,
+ param_decls=None,
+ type=None,
+ required=False,
+ default=None,
+ callback=None,
+ nargs=None,
+ metavar=None,
+ expose_value=True,
+ is_eager=False,
+ envvar=None,
+ autocompletion=None,
+ ):
+ self.name, self.opts, self.secondary_opts = self._parse_decls(
+ param_decls or (), expose_value
+ )
+
+ self.type = convert_type(type, default)
+
+ # Default nargs to what the type tells us if we have that
+ # information available.
+ if nargs is None:
+ if self.type.is_composite:
+ nargs = self.type.arity
+ else:
+ nargs = 1
+
+ self.required = required
+ self.callback = callback
+ self.nargs = nargs
+ self.multiple = False
+ self.expose_value = expose_value
+ self.default = default
+ self.is_eager = is_eager
+ self.metavar = metavar
+ self.envvar = envvar
+ self.autocompletion = autocompletion
+
+ def __repr__(self):
+ return "<{} {}>".format(self.__class__.__name__, self.name)
+
+ @property
+ def human_readable_name(self):
+ """Returns the human readable name of this parameter. This is the
+ same as the name for options, but the metavar for arguments.
+ """
+ return self.name
+
+ def make_metavar(self):
+ if self.metavar is not None:
+ return self.metavar
+ metavar = self.type.get_metavar(self)
+ if metavar is None:
+ metavar = self.type.name.upper()
+ if self.nargs != 1:
+ metavar += "..."
+ return metavar
+
+ def get_default(self, ctx):
+ """Given a context variable this calculates the default value."""
+ # Otherwise go with the regular default.
+ if callable(self.default):
+ rv = self.default()
+ else:
+ rv = self.default
+ return self.type_cast_value(ctx, rv)
+
+ def add_to_parser(self, parser, ctx):
+ pass
+
+ def consume_value(self, ctx, opts):
+ value = opts.get(self.name)
+ if value is None:
+ value = self.value_from_envvar(ctx)
+ if value is None:
+ value = ctx.lookup_default(self.name)
+ return value
+
+ def type_cast_value(self, ctx, value):
+ """Given a value this runs it properly through the type system.
+ This automatically handles things like `nargs` and `multiple` as
+ well as composite types.
+ """
+ if self.type.is_composite:
+ if self.nargs <= 1:
+ raise TypeError(
+ "Attempted to invoke composite type but nargs has"
+ " been set to {}. This is not supported; nargs"
+ " needs to be set to a fixed value > 1.".format(self.nargs)
+ )
+ if self.multiple:
+ return tuple(self.type(x or (), self, ctx) for x in value or ())
+ return self.type(value or (), self, ctx)
+
+ def _convert(value, level):
+ if level == 0:
+ return self.type(value, self, ctx)
+ return tuple(_convert(x, level - 1) for x in value or ())
+
+ return _convert(value, (self.nargs != 1) + bool(self.multiple))
+
+ def process_value(self, ctx, value):
+ """Given a value and context this runs the logic to convert the
+ value as necessary.
+ """
+ # If the value we were given is None we do nothing. This way
+ # code that calls this can easily figure out if something was
+ # not provided. Otherwise it would be converted into an empty
+ # tuple for multiple invocations which is inconvenient.
+ if value is not None:
+ return self.type_cast_value(ctx, value)
+
+ def value_is_missing(self, value):
+ if value is None:
+ return True
+ if (self.nargs != 1 or self.multiple) and value == ():
+ return True
+ return False
+
+ def full_process_value(self, ctx, value):
+ value = self.process_value(ctx, value)
+
+ if value is None and not ctx.resilient_parsing:
+ value = self.get_default(ctx)
+
+ if self.required and self.value_is_missing(value):
+ raise MissingParameter(ctx=ctx, param=self)
+
+ return value
+
+ def resolve_envvar_value(self, ctx):
+ if self.envvar is None:
+ return
+ if isinstance(self.envvar, (tuple, list)):
+ for envvar in self.envvar:
+ rv = os.environ.get(envvar)
+ if rv is not None:
+ return rv
+ else:
+ rv = os.environ.get(self.envvar)
+
+ if rv != "":
+ return rv
+
+ def value_from_envvar(self, ctx):
+ rv = self.resolve_envvar_value(ctx)
+ if rv is not None and self.nargs != 1:
+ rv = self.type.split_envvar_value(rv)
+ return rv
+
+ def handle_parse_result(self, ctx, opts, args):
+ with augment_usage_errors(ctx, param=self):
+ value = self.consume_value(ctx, opts)
+ try:
+ value = self.full_process_value(ctx, value)
+ except Exception:
+ if not ctx.resilient_parsing:
+ raise
+ value = None
+ if self.callback is not None:
+ try:
+ value = invoke_param_callback(self.callback, ctx, self, value)
+ except Exception:
+ if not ctx.resilient_parsing:
+ raise
+
+ if self.expose_value:
+ ctx.params[self.name] = value
+ return value, args
+
+ def get_help_record(self, ctx):
+ pass
+
+ def get_usage_pieces(self, ctx):
+ return []
+
+ def get_error_hint(self, ctx):
+ """Get a stringified version of the param for use in error messages to
+ indicate which param caused the error.
+ """
+ hint_list = self.opts or [self.human_readable_name]
+ return " / ".join(repr(x) for x in hint_list)
+
+
+class Option(Parameter):
+ """Options are usually optional values on the command line and
+ have some extra features that arguments don't have.
+
+ All other parameters are passed onwards to the parameter constructor.
+
+ :param show_default: controls if the default value should be shown on the
+ help page. Normally, defaults are not shown. If this
+ value is a string, it shows the string instead of the
+ value. This is particularly useful for dynamic options.
+ :param show_envvar: controls if an environment variable should be shown on
+ the help page. Normally, environment variables
+ are not shown.
+ :param prompt: if set to `True` or a non empty string then the user will be
+ prompted for input. If set to `True` the prompt will be the
+ option name capitalized.
+ :param confirmation_prompt: if set then the value will need to be confirmed
+ if it was prompted for.
+ :param hide_input: if this is `True` then the input on the prompt will be
+ hidden from the user. This is useful for password
+ input.
+ :param is_flag: forces this option to act as a flag. The default is
+ auto detection.
+ :param flag_value: which value should be used for this flag if it's
+ enabled. This is set to a boolean automatically if
+ the option string contains a slash to mark two options.
+ :param multiple: if this is set to `True` then the argument is accepted
+ multiple times and recorded. This is similar to ``nargs``
+ in how it works but supports arbitrary number of
+ arguments.
+ :param count: this flag makes an option increment an integer.
+ :param allow_from_autoenv: if this is enabled then the value of this
+ parameter will be pulled from an environment
+ variable in case a prefix is defined on the
+ context.
+ :param help: the help string.
+ :param hidden: hide this option from help outputs.
+ """
+
+ param_type_name = "option"
+
+ def __init__(
+ self,
+ param_decls=None,
+ show_default=False,
+ prompt=False,
+ confirmation_prompt=False,
+ hide_input=False,
+ is_flag=None,
+ flag_value=None,
+ multiple=False,
+ count=False,
+ allow_from_autoenv=True,
+ type=None,
+ help=None,
+ hidden=False,
+ show_choices=True,
+ show_envvar=False,
+ **attrs
+ ):
+ default_is_missing = attrs.get("default", _missing) is _missing
+ Parameter.__init__(self, param_decls, type=type, **attrs)
+
+ if prompt is True:
+ prompt_text = self.name.replace("_", " ").capitalize()
+ elif prompt is False:
+ prompt_text = None
+ else:
+ prompt_text = prompt
+ self.prompt = prompt_text
+ self.confirmation_prompt = confirmation_prompt
+ self.hide_input = hide_input
+ self.hidden = hidden
+
+ # Flags
+ if is_flag is None:
+ if flag_value is not None:
+ is_flag = True
+ else:
+ is_flag = bool(self.secondary_opts)
+ if is_flag and default_is_missing:
+ self.default = False
+ if flag_value is None:
+ flag_value = not self.default
+ self.is_flag = is_flag
+ self.flag_value = flag_value
+ if self.is_flag and isinstance(self.flag_value, bool) and type in [None, bool]:
+ self.type = BOOL
+ self.is_bool_flag = True
+ else:
+ self.is_bool_flag = False
+
+ # Counting
+ self.count = count
+ if count:
+ if type is None:
+ self.type = IntRange(min=0)
+ if default_is_missing:
+ self.default = 0
+
+ self.multiple = multiple
+ self.allow_from_autoenv = allow_from_autoenv
+ self.help = help
+ self.show_default = show_default
+ self.show_choices = show_choices
+ self.show_envvar = show_envvar
+
+ # Sanity check for stuff we don't support
+ if __debug__:
+ if self.nargs < 0:
+ raise TypeError("Options cannot have nargs < 0")
+ if self.prompt and self.is_flag and not self.is_bool_flag:
+ raise TypeError("Cannot prompt for flags that are not bools.")
+ if not self.is_bool_flag and self.secondary_opts:
+ raise TypeError("Got secondary option for non boolean flag.")
+ if self.is_bool_flag and self.hide_input and self.prompt is not None:
+ raise TypeError("Hidden input does not work with boolean flag prompts.")
+ if self.count:
+ if self.multiple:
+ raise TypeError(
+ "Options cannot be multiple and count at the same time."
+ )
+ elif self.is_flag:
+ raise TypeError(
+ "Options cannot be count and flags at the same time."
+ )
+
+ def _parse_decls(self, decls, expose_value):
+ opts = []
+ secondary_opts = []
+ name = None
+ possible_names = []
+
+ for decl in decls:
+ if isidentifier(decl):
+ if name is not None:
+ raise TypeError("Name defined twice")
+ name = decl
+ else:
+ split_char = ";" if decl[:1] == "/" else "/"
+ if split_char in decl:
+ first, second = decl.split(split_char, 1)
+ first = first.rstrip()
+ if first:
+ possible_names.append(split_opt(first))
+ opts.append(first)
+ second = second.lstrip()
+ if second:
+ secondary_opts.append(second.lstrip())
+ else:
+ possible_names.append(split_opt(decl))
+ opts.append(decl)
+
+ if name is None and possible_names:
+ possible_names.sort(key=lambda x: -len(x[0])) # group long options first
+ name = possible_names[0][1].replace("-", "_").lower()
+ if not isidentifier(name):
+ name = None
+
+ if name is None:
+ if not expose_value:
+ return None, opts, secondary_opts
+ raise TypeError("Could not determine name for option")
+
+ if not opts and not secondary_opts:
+ raise TypeError(
+ "No options defined but a name was passed ({}). Did you"
+ " mean to declare an argument instead of an option?".format(name)
+ )
+
+ return name, opts, secondary_opts
+
+ def add_to_parser(self, parser, ctx):
+ kwargs = {
+ "dest": self.name,
+ "nargs": self.nargs,
+ "obj": self,
+ }
+
+ if self.multiple:
+ action = "append"
+ elif self.count:
+ action = "count"
+ else:
+ action = "store"
+
+ if self.is_flag:
+ kwargs.pop("nargs", None)
+ action_const = "{}_const".format(action)
+ if self.is_bool_flag and self.secondary_opts:
+ parser.add_option(self.opts, action=action_const, const=True, **kwargs)
+ parser.add_option(
+ self.secondary_opts, action=action_const, const=False, **kwargs
+ )
+ else:
+ parser.add_option(
+ self.opts, action=action_const, const=self.flag_value, **kwargs
+ )
+ else:
+ kwargs["action"] = action
+ parser.add_option(self.opts, **kwargs)
+
+ def get_help_record(self, ctx):
+ if self.hidden:
+ return
+ any_prefix_is_slash = []
+
+ def _write_opts(opts):
+ rv, any_slashes = join_options(opts)
+ if any_slashes:
+ any_prefix_is_slash[:] = [True]
+ if not self.is_flag and not self.count:
+ rv += " {}".format(self.make_metavar())
+ return rv
+
+ rv = [_write_opts(self.opts)]
+ if self.secondary_opts:
+ rv.append(_write_opts(self.secondary_opts))
+
+ help = self.help or ""
+ extra = []
+ if self.show_envvar:
+ envvar = self.envvar
+ if envvar is None:
+ if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None:
+ envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper())
+ if envvar is not None:
+ extra.append(
+ "env var: {}".format(
+ ", ".join(str(d) for d in envvar)
+ if isinstance(envvar, (list, tuple))
+ else envvar
+ )
+ )
+ if self.default is not None and (self.show_default or ctx.show_default):
+ if isinstance(self.show_default, string_types):
+ default_string = "({})".format(self.show_default)
+ elif isinstance(self.default, (list, tuple)):
+ default_string = ", ".join(str(d) for d in self.default)
+ elif inspect.isfunction(self.default):
+ default_string = "(dynamic)"
+ else:
+ default_string = self.default
+ extra.append("default: {}".format(default_string))
+
+ if self.required:
+ extra.append("required")
+ if extra:
+ help = "{}[{}]".format(
+ "{} ".format(help) if help else "", "; ".join(extra)
+ )
+
+ return ("; " if any_prefix_is_slash else " / ").join(rv), help
+
+ def get_default(self, ctx):
+ # If we're a non boolean flag our default is more complex because
+ # we need to look at all flags in the same group to figure out
+ # if we're the the default one in which case we return the flag
+ # value as default.
+ if self.is_flag and not self.is_bool_flag:
+ for param in ctx.command.params:
+ if param.name == self.name and param.default:
+ return param.flag_value
+ return None
+ return Parameter.get_default(self, ctx)
+
+ def prompt_for_value(self, ctx):
+ """This is an alternative flow that can be activated in the full
+ value processing if a value does not exist. It will prompt the
+ user until a valid value exists and then returns the processed
+ value as result.
+ """
+ # Calculate the default before prompting anything to be stable.
+ default = self.get_default(ctx)
+
+ # If this is a prompt for a flag we need to handle this
+ # differently.
+ if self.is_bool_flag:
+ return confirm(self.prompt, default)
+
+ return prompt(
+ self.prompt,
+ default=default,
+ type=self.type,
+ hide_input=self.hide_input,
+ show_choices=self.show_choices,
+ confirmation_prompt=self.confirmation_prompt,
+ value_proc=lambda x: self.process_value(ctx, x),
+ )
+
+ def resolve_envvar_value(self, ctx):
+ rv = Parameter.resolve_envvar_value(self, ctx)
+ if rv is not None:
+ return rv
+ if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None:
+ envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper())
+ return os.environ.get(envvar)
+
+ def value_from_envvar(self, ctx):
+ rv = self.resolve_envvar_value(ctx)
+ if rv is None:
+ return None
+ value_depth = (self.nargs != 1) + bool(self.multiple)
+ if value_depth > 0 and rv is not None:
+ rv = self.type.split_envvar_value(rv)
+ if self.multiple and self.nargs != 1:
+ rv = batch(rv, self.nargs)
+ return rv
+
+ def full_process_value(self, ctx, value):
+ if value is None and self.prompt is not None and not ctx.resilient_parsing:
+ return self.prompt_for_value(ctx)
+ return Parameter.full_process_value(self, ctx, value)
+
+
+class Argument(Parameter):
+ """Arguments are positional parameters to a command. They generally
+ provide fewer features than options but can have infinite ``nargs``
+ and are required by default.
+
+ All parameters are passed onwards to the parameter constructor.
+ """
+
+ param_type_name = "argument"
+
+ def __init__(self, param_decls, required=None, **attrs):
+ if required is None:
+ if attrs.get("default") is not None:
+ required = False
+ else:
+ required = attrs.get("nargs", 1) > 0
+ Parameter.__init__(self, param_decls, required=required, **attrs)
+ if self.default is not None and self.nargs < 0:
+ raise TypeError(
+ "nargs=-1 in combination with a default value is not supported."
+ )
+
+ @property
+ def human_readable_name(self):
+ if self.metavar is not None:
+ return self.metavar
+ return self.name.upper()
+
+ def make_metavar(self):
+ if self.metavar is not None:
+ return self.metavar
+ var = self.type.get_metavar(self)
+ if not var:
+ var = self.name.upper()
+ if not self.required:
+ var = "[{}]".format(var)
+ if self.nargs != 1:
+ var += "..."
+ return var
+
+ def _parse_decls(self, decls, expose_value):
+ if not decls:
+ if not expose_value:
+ return None, [], []
+ raise TypeError("Could not determine name for argument")
+ if len(decls) == 1:
+ name = arg = decls[0]
+ name = name.replace("-", "_").lower()
+ else:
+ raise TypeError(
+ "Arguments take exactly one parameter declaration, got"
+ " {}".format(len(decls))
+ )
+ return name, [arg], []
+
+ def get_usage_pieces(self, ctx):
+ return [self.make_metavar()]
+
+ def get_error_hint(self, ctx):
+ return repr(self.make_metavar())
+
+ def add_to_parser(self, parser, ctx):
+ parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)
diff --git a/mixly/tools/python/click/decorators.py b/mixly/tools/python/click/decorators.py
new file mode 100644
index 00000000..c7b5af6c
--- /dev/null
+++ b/mixly/tools/python/click/decorators.py
@@ -0,0 +1,333 @@
+import inspect
+import sys
+from functools import update_wrapper
+
+from ._compat import iteritems
+from ._unicodefun import _check_for_unicode_literals
+from .core import Argument
+from .core import Command
+from .core import Group
+from .core import Option
+from .globals import get_current_context
+from .utils import echo
+
+
+def pass_context(f):
+ """Marks a callback as wanting to receive the current context
+ object as first argument.
+ """
+
+ def new_func(*args, **kwargs):
+ return f(get_current_context(), *args, **kwargs)
+
+ return update_wrapper(new_func, f)
+
+
+def pass_obj(f):
+ """Similar to :func:`pass_context`, but only pass the object on the
+ context onwards (:attr:`Context.obj`). This is useful if that object
+ represents the state of a nested system.
+ """
+
+ def new_func(*args, **kwargs):
+ return f(get_current_context().obj, *args, **kwargs)
+
+ return update_wrapper(new_func, f)
+
+
+def make_pass_decorator(object_type, ensure=False):
+ """Given an object type this creates a decorator that will work
+ similar to :func:`pass_obj` but instead of passing the object of the
+ current context, it will find the innermost context of type
+ :func:`object_type`.
+
+ This generates a decorator that works roughly like this::
+
+ from functools import update_wrapper
+
+ def decorator(f):
+ @pass_context
+ def new_func(ctx, *args, **kwargs):
+ obj = ctx.find_object(object_type)
+ return ctx.invoke(f, obj, *args, **kwargs)
+ return update_wrapper(new_func, f)
+ return decorator
+
+ :param object_type: the type of the object to pass.
+ :param ensure: if set to `True`, a new object will be created and
+ remembered on the context if it's not there yet.
+ """
+
+ def decorator(f):
+ def new_func(*args, **kwargs):
+ ctx = get_current_context()
+ if ensure:
+ obj = ctx.ensure_object(object_type)
+ else:
+ obj = ctx.find_object(object_type)
+ if obj is None:
+ raise RuntimeError(
+ "Managed to invoke callback without a context"
+ " object of type '{}' existing".format(object_type.__name__)
+ )
+ return ctx.invoke(f, obj, *args, **kwargs)
+
+ return update_wrapper(new_func, f)
+
+ return decorator
+
+
+def _make_command(f, name, attrs, cls):
+ if isinstance(f, Command):
+ raise TypeError("Attempted to convert a callback into a command twice.")
+ try:
+ params = f.__click_params__
+ params.reverse()
+ del f.__click_params__
+ except AttributeError:
+ params = []
+ help = attrs.get("help")
+ if help is None:
+ help = inspect.getdoc(f)
+ if isinstance(help, bytes):
+ help = help.decode("utf-8")
+ else:
+ help = inspect.cleandoc(help)
+ attrs["help"] = help
+ _check_for_unicode_literals()
+ return cls(
+ name=name or f.__name__.lower().replace("_", "-"),
+ callback=f,
+ params=params,
+ **attrs
+ )
+
+
+def command(name=None, cls=None, **attrs):
+ r"""Creates a new :class:`Command` and uses the decorated function as
+ callback. This will also automatically attach all decorated
+ :func:`option`\s and :func:`argument`\s as parameters to the command.
+
+ The name of the command defaults to the name of the function with
+ underscores replaced by dashes. If you want to change that, you can
+ pass the intended name as the first argument.
+
+ All keyword arguments are forwarded to the underlying command class.
+
+ Once decorated the function turns into a :class:`Command` instance
+ that can be invoked as a command line utility or be attached to a
+ command :class:`Group`.
+
+ :param name: the name of the command. This defaults to the function
+ name with underscores replaced by dashes.
+ :param cls: the command class to instantiate. This defaults to
+ :class:`Command`.
+ """
+ if cls is None:
+ cls = Command
+
+ def decorator(f):
+ cmd = _make_command(f, name, attrs, cls)
+ cmd.__doc__ = f.__doc__
+ return cmd
+
+ return decorator
+
+
+def group(name=None, **attrs):
+ """Creates a new :class:`Group` with a function as callback. This
+ works otherwise the same as :func:`command` just that the `cls`
+ parameter is set to :class:`Group`.
+ """
+ attrs.setdefault("cls", Group)
+ return command(name, **attrs)
+
+
+def _param_memo(f, param):
+ if isinstance(f, Command):
+ f.params.append(param)
+ else:
+ if not hasattr(f, "__click_params__"):
+ f.__click_params__ = []
+ f.__click_params__.append(param)
+
+
+def argument(*param_decls, **attrs):
+ """Attaches an argument to the command. All positional arguments are
+ passed as parameter declarations to :class:`Argument`; all keyword
+ arguments are forwarded unchanged (except ``cls``).
+ This is equivalent to creating an :class:`Argument` instance manually
+ and attaching it to the :attr:`Command.params` list.
+
+ :param cls: the argument class to instantiate. This defaults to
+ :class:`Argument`.
+ """
+
+ def decorator(f):
+ ArgumentClass = attrs.pop("cls", Argument)
+ _param_memo(f, ArgumentClass(param_decls, **attrs))
+ return f
+
+ return decorator
+
+
+def option(*param_decls, **attrs):
+ """Attaches an option to the command. All positional arguments are
+ passed as parameter declarations to :class:`Option`; all keyword
+ arguments are forwarded unchanged (except ``cls``).
+ This is equivalent to creating an :class:`Option` instance manually
+ and attaching it to the :attr:`Command.params` list.
+
+ :param cls: the option class to instantiate. This defaults to
+ :class:`Option`.
+ """
+
+ def decorator(f):
+ # Issue 926, copy attrs, so pre-defined options can re-use the same cls=
+ option_attrs = attrs.copy()
+
+ if "help" in option_attrs:
+ option_attrs["help"] = inspect.cleandoc(option_attrs["help"])
+ OptionClass = option_attrs.pop("cls", Option)
+ _param_memo(f, OptionClass(param_decls, **option_attrs))
+ return f
+
+ return decorator
+
+
+def confirmation_option(*param_decls, **attrs):
+ """Shortcut for confirmation prompts that can be ignored by passing
+ ``--yes`` as parameter.
+
+ This is equivalent to decorating a function with :func:`option` with
+ the following parameters::
+
+ def callback(ctx, param, value):
+ if not value:
+ ctx.abort()
+
+ @click.command()
+ @click.option('--yes', is_flag=True, callback=callback,
+ expose_value=False, prompt='Do you want to continue?')
+ def dropdb():
+ pass
+ """
+
+ def decorator(f):
+ def callback(ctx, param, value):
+ if not value:
+ ctx.abort()
+
+ attrs.setdefault("is_flag", True)
+ attrs.setdefault("callback", callback)
+ attrs.setdefault("expose_value", False)
+ attrs.setdefault("prompt", "Do you want to continue?")
+ attrs.setdefault("help", "Confirm the action without prompting.")
+ return option(*(param_decls or ("--yes",)), **attrs)(f)
+
+ return decorator
+
+
+def password_option(*param_decls, **attrs):
+ """Shortcut for password prompts.
+
+ This is equivalent to decorating a function with :func:`option` with
+ the following parameters::
+
+ @click.command()
+ @click.option('--password', prompt=True, confirmation_prompt=True,
+ hide_input=True)
+ def changeadmin(password):
+ pass
+ """
+
+ def decorator(f):
+ attrs.setdefault("prompt", True)
+ attrs.setdefault("confirmation_prompt", True)
+ attrs.setdefault("hide_input", True)
+ return option(*(param_decls or ("--password",)), **attrs)(f)
+
+ return decorator
+
+
+def version_option(version=None, *param_decls, **attrs):
+ """Adds a ``--version`` option which immediately ends the program
+ printing out the version number. This is implemented as an eager
+ option that prints the version and exits the program in the callback.
+
+ :param version: the version number to show. If not provided Click
+ attempts an auto discovery via setuptools.
+ :param prog_name: the name of the program (defaults to autodetection)
+ :param message: custom message to show instead of the default
+ (``'%(prog)s, version %(version)s'``)
+ :param others: everything else is forwarded to :func:`option`.
+ """
+ if version is None:
+ if hasattr(sys, "_getframe"):
+ module = sys._getframe(1).f_globals.get("__name__")
+ else:
+ module = ""
+
+ def decorator(f):
+ prog_name = attrs.pop("prog_name", None)
+ message = attrs.pop("message", "%(prog)s, version %(version)s")
+
+ def callback(ctx, param, value):
+ if not value or ctx.resilient_parsing:
+ return
+ prog = prog_name
+ if prog is None:
+ prog = ctx.find_root().info_name
+ ver = version
+ if ver is None:
+ try:
+ import pkg_resources
+ except ImportError:
+ pass
+ else:
+ for dist in pkg_resources.working_set:
+ scripts = dist.get_entry_map().get("console_scripts") or {}
+ for _, entry_point in iteritems(scripts):
+ if entry_point.module_name == module:
+ ver = dist.version
+ break
+ if ver is None:
+ raise RuntimeError("Could not determine version")
+ echo(message % {"prog": prog, "version": ver}, color=ctx.color)
+ ctx.exit()
+
+ attrs.setdefault("is_flag", True)
+ attrs.setdefault("expose_value", False)
+ attrs.setdefault("is_eager", True)
+ attrs.setdefault("help", "Show the version and exit.")
+ attrs["callback"] = callback
+ return option(*(param_decls or ("--version",)), **attrs)(f)
+
+ return decorator
+
+
+def help_option(*param_decls, **attrs):
+ """Adds a ``--help`` option which immediately ends the program
+ printing out the help page. This is usually unnecessary to add as
+ this is added by default to all commands unless suppressed.
+
+ Like :func:`version_option`, this is implemented as eager option that
+ prints in the callback and exits.
+
+ All arguments are forwarded to :func:`option`.
+ """
+
+ def decorator(f):
+ def callback(ctx, param, value):
+ if value and not ctx.resilient_parsing:
+ echo(ctx.get_help(), color=ctx.color)
+ ctx.exit()
+
+ attrs.setdefault("is_flag", True)
+ attrs.setdefault("expose_value", False)
+ attrs.setdefault("help", "Show this message and exit.")
+ attrs.setdefault("is_eager", True)
+ attrs["callback"] = callback
+ return option(*(param_decls or ("--help",)), **attrs)(f)
+
+ return decorator
diff --git a/mixly/tools/python/click/exceptions.py b/mixly/tools/python/click/exceptions.py
new file mode 100644
index 00000000..592ee38f
--- /dev/null
+++ b/mixly/tools/python/click/exceptions.py
@@ -0,0 +1,253 @@
+from ._compat import filename_to_ui
+from ._compat import get_text_stderr
+from ._compat import PY2
+from .utils import echo
+
+
+def _join_param_hints(param_hint):
+ if isinstance(param_hint, (tuple, list)):
+ return " / ".join(repr(x) for x in param_hint)
+ return param_hint
+
+
+class ClickException(Exception):
+ """An exception that Click can handle and show to the user."""
+
+ #: The exit code for this exception
+ exit_code = 1
+
+ def __init__(self, message):
+ ctor_msg = message
+ if PY2:
+ if ctor_msg is not None:
+ ctor_msg = ctor_msg.encode("utf-8")
+ Exception.__init__(self, ctor_msg)
+ self.message = message
+
+ def format_message(self):
+ return self.message
+
+ def __str__(self):
+ return self.message
+
+ if PY2:
+ __unicode__ = __str__
+
+ def __str__(self):
+ return self.message.encode("utf-8")
+
+ def show(self, file=None):
+ if file is None:
+ file = get_text_stderr()
+ echo("Error: {}".format(self.format_message()), file=file)
+
+
+class UsageError(ClickException):
+ """An internal exception that signals a usage error. This typically
+ aborts any further handling.
+
+ :param message: the error message to display.
+ :param ctx: optionally the context that caused this error. Click will
+ fill in the context automatically in some situations.
+ """
+
+ exit_code = 2
+
+ def __init__(self, message, ctx=None):
+ ClickException.__init__(self, message)
+ self.ctx = ctx
+ self.cmd = self.ctx.command if self.ctx else None
+
+ def show(self, file=None):
+ if file is None:
+ file = get_text_stderr()
+ color = None
+ hint = ""
+ if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None:
+ hint = "Try '{} {}' for help.\n".format(
+ self.ctx.command_path, self.ctx.help_option_names[0]
+ )
+ if self.ctx is not None:
+ color = self.ctx.color
+ echo("{}\n{}".format(self.ctx.get_usage(), hint), file=file, color=color)
+ echo("Error: {}".format(self.format_message()), file=file, color=color)
+
+
+class BadParameter(UsageError):
+ """An exception that formats out a standardized error message for a
+ bad parameter. This is useful when thrown from a callback or type as
+ Click will attach contextual information to it (for instance, which
+ parameter it is).
+
+ .. versionadded:: 2.0
+
+ :param param: the parameter object that caused this error. This can
+ be left out, and Click will attach this info itself
+ if possible.
+ :param param_hint: a string that shows up as parameter name. This
+ can be used as alternative to `param` in cases
+ where custom validation should happen. If it is
+ a string it's used as such, if it's a list then
+ each item is quoted and separated.
+ """
+
+ def __init__(self, message, ctx=None, param=None, param_hint=None):
+ UsageError.__init__(self, message, ctx)
+ self.param = param
+ self.param_hint = param_hint
+
+ def format_message(self):
+ if self.param_hint is not None:
+ param_hint = self.param_hint
+ elif self.param is not None:
+ param_hint = self.param.get_error_hint(self.ctx)
+ else:
+ return "Invalid value: {}".format(self.message)
+ param_hint = _join_param_hints(param_hint)
+
+ return "Invalid value for {}: {}".format(param_hint, self.message)
+
+
+class MissingParameter(BadParameter):
+ """Raised if click required an option or argument but it was not
+ provided when invoking the script.
+
+ .. versionadded:: 4.0
+
+ :param param_type: a string that indicates the type of the parameter.
+ The default is to inherit the parameter type from
+ the given `param`. Valid values are ``'parameter'``,
+ ``'option'`` or ``'argument'``.
+ """
+
+ def __init__(
+ self, message=None, ctx=None, param=None, param_hint=None, param_type=None
+ ):
+ BadParameter.__init__(self, message, ctx, param, param_hint)
+ self.param_type = param_type
+
+ def format_message(self):
+ if self.param_hint is not None:
+ param_hint = self.param_hint
+ elif self.param is not None:
+ param_hint = self.param.get_error_hint(self.ctx)
+ else:
+ param_hint = None
+ param_hint = _join_param_hints(param_hint)
+
+ param_type = self.param_type
+ if param_type is None and self.param is not None:
+ param_type = self.param.param_type_name
+
+ msg = self.message
+ if self.param is not None:
+ msg_extra = self.param.type.get_missing_message(self.param)
+ if msg_extra:
+ if msg:
+ msg += ". {}".format(msg_extra)
+ else:
+ msg = msg_extra
+
+ return "Missing {}{}{}{}".format(
+ param_type,
+ " {}".format(param_hint) if param_hint else "",
+ ". " if msg else ".",
+ msg or "",
+ )
+
+ def __str__(self):
+ if self.message is None:
+ param_name = self.param.name if self.param else None
+ return "missing parameter: {}".format(param_name)
+ else:
+ return self.message
+
+ if PY2:
+ __unicode__ = __str__
+
+ def __str__(self):
+ return self.__unicode__().encode("utf-8")
+
+
+class NoSuchOption(UsageError):
+ """Raised if click attempted to handle an option that does not
+ exist.
+
+ .. versionadded:: 4.0
+ """
+
+ def __init__(self, option_name, message=None, possibilities=None, ctx=None):
+ if message is None:
+ message = "no such option: {}".format(option_name)
+ UsageError.__init__(self, message, ctx)
+ self.option_name = option_name
+ self.possibilities = possibilities
+
+ def format_message(self):
+ bits = [self.message]
+ if self.possibilities:
+ if len(self.possibilities) == 1:
+ bits.append("Did you mean {}?".format(self.possibilities[0]))
+ else:
+ possibilities = sorted(self.possibilities)
+ bits.append("(Possible options: {})".format(", ".join(possibilities)))
+ return " ".join(bits)
+
+
+class BadOptionUsage(UsageError):
+ """Raised if an option is generally supplied but the use of the option
+ was incorrect. This is for instance raised if the number of arguments
+ for an option is not correct.
+
+ .. versionadded:: 4.0
+
+ :param option_name: the name of the option being used incorrectly.
+ """
+
+ def __init__(self, option_name, message, ctx=None):
+ UsageError.__init__(self, message, ctx)
+ self.option_name = option_name
+
+
+class BadArgumentUsage(UsageError):
+ """Raised if an argument is generally supplied but the use of the argument
+ was incorrect. This is for instance raised if the number of values
+ for an argument is not correct.
+
+ .. versionadded:: 6.0
+ """
+
+ def __init__(self, message, ctx=None):
+ UsageError.__init__(self, message, ctx)
+
+
+class FileError(ClickException):
+ """Raised if a file cannot be opened."""
+
+ def __init__(self, filename, hint=None):
+ ui_filename = filename_to_ui(filename)
+ if hint is None:
+ hint = "unknown error"
+ ClickException.__init__(self, hint)
+ self.ui_filename = ui_filename
+ self.filename = filename
+
+ def format_message(self):
+ return "Could not open file {}: {}".format(self.ui_filename, self.message)
+
+
+class Abort(RuntimeError):
+ """An internal signalling exception that signals Click to abort."""
+
+
+class Exit(RuntimeError):
+ """An exception that indicates that the application should exit with some
+ status code.
+
+ :param code: the status code to exit with.
+ """
+
+ __slots__ = ("exit_code",)
+
+ def __init__(self, code=0):
+ self.exit_code = code
diff --git a/mixly/tools/python/click/formatting.py b/mixly/tools/python/click/formatting.py
new file mode 100644
index 00000000..319c7f61
--- /dev/null
+++ b/mixly/tools/python/click/formatting.py
@@ -0,0 +1,283 @@
+from contextlib import contextmanager
+
+from ._compat import term_len
+from .parser import split_opt
+from .termui import get_terminal_size
+
+# Can force a width. This is used by the test system
+FORCED_WIDTH = None
+
+
+def measure_table(rows):
+ widths = {}
+ for row in rows:
+ for idx, col in enumerate(row):
+ widths[idx] = max(widths.get(idx, 0), term_len(col))
+ return tuple(y for x, y in sorted(widths.items()))
+
+
+def iter_rows(rows, col_count):
+ for row in rows:
+ row = tuple(row)
+ yield row + ("",) * (col_count - len(row))
+
+
+def wrap_text(
+ text, width=78, initial_indent="", subsequent_indent="", preserve_paragraphs=False
+):
+ """A helper function that intelligently wraps text. By default, it
+ assumes that it operates on a single paragraph of text but if the
+ `preserve_paragraphs` parameter is provided it will intelligently
+ handle paragraphs (defined by two empty lines).
+
+ If paragraphs are handled, a paragraph can be prefixed with an empty
+ line containing the ``\\b`` character (``\\x08``) to indicate that
+ no rewrapping should happen in that block.
+
+ :param text: the text that should be rewrapped.
+ :param width: the maximum width for the text.
+ :param initial_indent: the initial indent that should be placed on the
+ first line as a string.
+ :param subsequent_indent: the indent string that should be placed on
+ each consecutive line.
+ :param preserve_paragraphs: if this flag is set then the wrapping will
+ intelligently handle paragraphs.
+ """
+ from ._textwrap import TextWrapper
+
+ text = text.expandtabs()
+ wrapper = TextWrapper(
+ width,
+ initial_indent=initial_indent,
+ subsequent_indent=subsequent_indent,
+ replace_whitespace=False,
+ )
+ if not preserve_paragraphs:
+ return wrapper.fill(text)
+
+ p = []
+ buf = []
+ indent = None
+
+ def _flush_par():
+ if not buf:
+ return
+ if buf[0].strip() == "\b":
+ p.append((indent or 0, True, "\n".join(buf[1:])))
+ else:
+ p.append((indent or 0, False, " ".join(buf)))
+ del buf[:]
+
+ for line in text.splitlines():
+ if not line:
+ _flush_par()
+ indent = None
+ else:
+ if indent is None:
+ orig_len = term_len(line)
+ line = line.lstrip()
+ indent = orig_len - term_len(line)
+ buf.append(line)
+ _flush_par()
+
+ rv = []
+ for indent, raw, text in p:
+ with wrapper.extra_indent(" " * indent):
+ if raw:
+ rv.append(wrapper.indent_only(text))
+ else:
+ rv.append(wrapper.fill(text))
+
+ return "\n\n".join(rv)
+
+
+class HelpFormatter(object):
+ """This class helps with formatting text-based help pages. It's
+ usually just needed for very special internal cases, but it's also
+ exposed so that developers can write their own fancy outputs.
+
+ At present, it always writes into memory.
+
+ :param indent_increment: the additional increment for each level.
+ :param width: the width for the text. This defaults to the terminal
+ width clamped to a maximum of 78.
+ """
+
+ def __init__(self, indent_increment=2, width=None, max_width=None):
+ self.indent_increment = indent_increment
+ if max_width is None:
+ max_width = 80
+ if width is None:
+ width = FORCED_WIDTH
+ if width is None:
+ width = max(min(get_terminal_size()[0], max_width) - 2, 50)
+ self.width = width
+ self.current_indent = 0
+ self.buffer = []
+
+ def write(self, string):
+ """Writes a unicode string into the internal buffer."""
+ self.buffer.append(string)
+
+ def indent(self):
+ """Increases the indentation."""
+ self.current_indent += self.indent_increment
+
+ def dedent(self):
+ """Decreases the indentation."""
+ self.current_indent -= self.indent_increment
+
+ def write_usage(self, prog, args="", prefix="Usage: "):
+ """Writes a usage line into the buffer.
+
+ :param prog: the program name.
+ :param args: whitespace separated list of arguments.
+ :param prefix: the prefix for the first line.
+ """
+ usage_prefix = "{:>{w}}{} ".format(prefix, prog, w=self.current_indent)
+ text_width = self.width - self.current_indent
+
+ if text_width >= (term_len(usage_prefix) + 20):
+ # The arguments will fit to the right of the prefix.
+ indent = " " * term_len(usage_prefix)
+ self.write(
+ wrap_text(
+ args,
+ text_width,
+ initial_indent=usage_prefix,
+ subsequent_indent=indent,
+ )
+ )
+ else:
+ # The prefix is too long, put the arguments on the next line.
+ self.write(usage_prefix)
+ self.write("\n")
+ indent = " " * (max(self.current_indent, term_len(prefix)) + 4)
+ self.write(
+ wrap_text(
+ args, text_width, initial_indent=indent, subsequent_indent=indent
+ )
+ )
+
+ self.write("\n")
+
+ def write_heading(self, heading):
+ """Writes a heading into the buffer."""
+ self.write("{:>{w}}{}:\n".format("", heading, w=self.current_indent))
+
+ def write_paragraph(self):
+ """Writes a paragraph into the buffer."""
+ if self.buffer:
+ self.write("\n")
+
+ def write_text(self, text):
+ """Writes re-indented text into the buffer. This rewraps and
+ preserves paragraphs.
+ """
+ text_width = max(self.width - self.current_indent, 11)
+ indent = " " * self.current_indent
+ self.write(
+ wrap_text(
+ text,
+ text_width,
+ initial_indent=indent,
+ subsequent_indent=indent,
+ preserve_paragraphs=True,
+ )
+ )
+ self.write("\n")
+
+ def write_dl(self, rows, col_max=30, col_spacing=2):
+ """Writes a definition list into the buffer. This is how options
+ and commands are usually formatted.
+
+ :param rows: a list of two item tuples for the terms and values.
+ :param col_max: the maximum width of the first column.
+ :param col_spacing: the number of spaces between the first and
+ second column.
+ """
+ rows = list(rows)
+ widths = measure_table(rows)
+ if len(widths) != 2:
+ raise TypeError("Expected two columns for definition list")
+
+ first_col = min(widths[0], col_max) + col_spacing
+
+ for first, second in iter_rows(rows, len(widths)):
+ self.write("{:>{w}}{}".format("", first, w=self.current_indent))
+ if not second:
+ self.write("\n")
+ continue
+ if term_len(first) <= first_col - col_spacing:
+ self.write(" " * (first_col - term_len(first)))
+ else:
+ self.write("\n")
+ self.write(" " * (first_col + self.current_indent))
+
+ text_width = max(self.width - first_col - 2, 10)
+ wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True)
+ lines = wrapped_text.splitlines()
+
+ if lines:
+ self.write("{}\n".format(lines[0]))
+
+ for line in lines[1:]:
+ self.write(
+ "{:>{w}}{}\n".format(
+ "", line, w=first_col + self.current_indent
+ )
+ )
+
+ if len(lines) > 1:
+ # separate long help from next option
+ self.write("\n")
+ else:
+ self.write("\n")
+
+ @contextmanager
+ def section(self, name):
+ """Helpful context manager that writes a paragraph, a heading,
+ and the indents.
+
+ :param name: the section name that is written as heading.
+ """
+ self.write_paragraph()
+ self.write_heading(name)
+ self.indent()
+ try:
+ yield
+ finally:
+ self.dedent()
+
+ @contextmanager
+ def indentation(self):
+ """A context manager that increases the indentation."""
+ self.indent()
+ try:
+ yield
+ finally:
+ self.dedent()
+
+ def getvalue(self):
+ """Returns the buffer contents."""
+ return "".join(self.buffer)
+
+
+def join_options(options):
+ """Given a list of option strings this joins them in the most appropriate
+ way and returns them in the form ``(formatted_string,
+ any_prefix_is_slash)`` where the second item in the tuple is a flag that
+ indicates if any of the option prefixes was a slash.
+ """
+ rv = []
+ any_prefix_is_slash = False
+ for opt in options:
+ prefix = split_opt(opt)[0]
+ if prefix == "/":
+ any_prefix_is_slash = True
+ rv.append((len(prefix), opt))
+
+ rv.sort(key=lambda x: x[0])
+
+ rv = ", ".join(x[1] for x in rv)
+ return rv, any_prefix_is_slash
diff --git a/mixly/tools/python/click/globals.py b/mixly/tools/python/click/globals.py
new file mode 100644
index 00000000..1649f9a0
--- /dev/null
+++ b/mixly/tools/python/click/globals.py
@@ -0,0 +1,47 @@
+from threading import local
+
+_local = local()
+
+
+def get_current_context(silent=False):
+ """Returns the current click context. This can be used as a way to
+ access the current context object from anywhere. This is a more implicit
+ alternative to the :func:`pass_context` decorator. This function is
+ primarily useful for helpers such as :func:`echo` which might be
+ interested in changing its behavior based on the current context.
+
+ To push the current context, :meth:`Context.scope` can be used.
+
+ .. versionadded:: 5.0
+
+ :param silent: if set to `True` the return value is `None` if no context
+ is available. The default behavior is to raise a
+ :exc:`RuntimeError`.
+ """
+ try:
+ return _local.stack[-1]
+ except (AttributeError, IndexError):
+ if not silent:
+ raise RuntimeError("There is no active click context.")
+
+
+def push_context(ctx):
+ """Pushes a new context to the current stack."""
+ _local.__dict__.setdefault("stack", []).append(ctx)
+
+
+def pop_context():
+ """Removes the top level from the stack."""
+ _local.stack.pop()
+
+
+def resolve_color_default(color=None):
+ """"Internal helper to get the default value of the color flag. If a
+ value is passed it's returned unchanged, otherwise it's looked up from
+ the current context.
+ """
+ if color is not None:
+ return color
+ ctx = get_current_context(silent=True)
+ if ctx is not None:
+ return ctx.color
diff --git a/mixly/tools/python/click/parser.py b/mixly/tools/python/click/parser.py
new file mode 100644
index 00000000..f43ebfe9
--- /dev/null
+++ b/mixly/tools/python/click/parser.py
@@ -0,0 +1,428 @@
+# -*- coding: utf-8 -*-
+"""
+This module started out as largely a copy paste from the stdlib's
+optparse module with the features removed that we do not need from
+optparse because we implement them in Click on a higher level (for
+instance type handling, help formatting and a lot more).
+
+The plan is to remove more and more from here over time.
+
+The reason this is a different module and not optparse from the stdlib
+is that there are differences in 2.x and 3.x about the error messages
+generated and optparse in the stdlib uses gettext for no good reason
+and might cause us issues.
+
+Click uses parts of optparse written by Gregory P. Ward and maintained
+by the Python Software Foundation. This is limited to code in parser.py.
+
+Copyright 2001-2006 Gregory P. Ward. All rights reserved.
+Copyright 2002-2006 Python Software Foundation. All rights reserved.
+"""
+import re
+from collections import deque
+
+from .exceptions import BadArgumentUsage
+from .exceptions import BadOptionUsage
+from .exceptions import NoSuchOption
+from .exceptions import UsageError
+
+
+def _unpack_args(args, nargs_spec):
+ """Given an iterable of arguments and an iterable of nargs specifications,
+ it returns a tuple with all the unpacked arguments at the first index
+ and all remaining arguments as the second.
+
+ The nargs specification is the number of arguments that should be consumed
+ or `-1` to indicate that this position should eat up all the remainders.
+
+ Missing items are filled with `None`.
+ """
+ args = deque(args)
+ nargs_spec = deque(nargs_spec)
+ rv = []
+ spos = None
+
+ def _fetch(c):
+ try:
+ if spos is None:
+ return c.popleft()
+ else:
+ return c.pop()
+ except IndexError:
+ return None
+
+ while nargs_spec:
+ nargs = _fetch(nargs_spec)
+ if nargs == 1:
+ rv.append(_fetch(args))
+ elif nargs > 1:
+ x = [_fetch(args) for _ in range(nargs)]
+ # If we're reversed, we're pulling in the arguments in reverse,
+ # so we need to turn them around.
+ if spos is not None:
+ x.reverse()
+ rv.append(tuple(x))
+ elif nargs < 0:
+ if spos is not None:
+ raise TypeError("Cannot have two nargs < 0")
+ spos = len(rv)
+ rv.append(None)
+
+ # spos is the position of the wildcard (star). If it's not `None`,
+ # we fill it with the remainder.
+ if spos is not None:
+ rv[spos] = tuple(args)
+ args = []
+ rv[spos + 1 :] = reversed(rv[spos + 1 :])
+
+ return tuple(rv), list(args)
+
+
+def _error_opt_args(nargs, opt):
+ if nargs == 1:
+ raise BadOptionUsage(opt, "{} option requires an argument".format(opt))
+ raise BadOptionUsage(opt, "{} option requires {} arguments".format(opt, nargs))
+
+
+def split_opt(opt):
+ first = opt[:1]
+ if first.isalnum():
+ return "", opt
+ if opt[1:2] == first:
+ return opt[:2], opt[2:]
+ return first, opt[1:]
+
+
+def normalize_opt(opt, ctx):
+ if ctx is None or ctx.token_normalize_func is None:
+ return opt
+ prefix, opt = split_opt(opt)
+ return prefix + ctx.token_normalize_func(opt)
+
+
+def split_arg_string(string):
+ """Given an argument string this attempts to split it into small parts."""
+ rv = []
+ for match in re.finditer(
+ r"('([^'\\]*(?:\\.[^'\\]*)*)'|\"([^\"\\]*(?:\\.[^\"\\]*)*)\"|\S+)\s*",
+ string,
+ re.S,
+ ):
+ arg = match.group().strip()
+ if arg[:1] == arg[-1:] and arg[:1] in "\"'":
+ arg = arg[1:-1].encode("ascii", "backslashreplace").decode("unicode-escape")
+ try:
+ arg = type(string)(arg)
+ except UnicodeError:
+ pass
+ rv.append(arg)
+ return rv
+
+
+class Option(object):
+ def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None):
+ self._short_opts = []
+ self._long_opts = []
+ self.prefixes = set()
+
+ for opt in opts:
+ prefix, value = split_opt(opt)
+ if not prefix:
+ raise ValueError("Invalid start character for option ({})".format(opt))
+ self.prefixes.add(prefix[0])
+ if len(prefix) == 1 and len(value) == 1:
+ self._short_opts.append(opt)
+ else:
+ self._long_opts.append(opt)
+ self.prefixes.add(prefix)
+
+ if action is None:
+ action = "store"
+
+ self.dest = dest
+ self.action = action
+ self.nargs = nargs
+ self.const = const
+ self.obj = obj
+
+ @property
+ def takes_value(self):
+ return self.action in ("store", "append")
+
+ def process(self, value, state):
+ if self.action == "store":
+ state.opts[self.dest] = value
+ elif self.action == "store_const":
+ state.opts[self.dest] = self.const
+ elif self.action == "append":
+ state.opts.setdefault(self.dest, []).append(value)
+ elif self.action == "append_const":
+ state.opts.setdefault(self.dest, []).append(self.const)
+ elif self.action == "count":
+ state.opts[self.dest] = state.opts.get(self.dest, 0) + 1
+ else:
+ raise ValueError("unknown action '{}'".format(self.action))
+ state.order.append(self.obj)
+
+
+class Argument(object):
+ def __init__(self, dest, nargs=1, obj=None):
+ self.dest = dest
+ self.nargs = nargs
+ self.obj = obj
+
+ def process(self, value, state):
+ if self.nargs > 1:
+ holes = sum(1 for x in value if x is None)
+ if holes == len(value):
+ value = None
+ elif holes != 0:
+ raise BadArgumentUsage(
+ "argument {} takes {} values".format(self.dest, self.nargs)
+ )
+ state.opts[self.dest] = value
+ state.order.append(self.obj)
+
+
+class ParsingState(object):
+ def __init__(self, rargs):
+ self.opts = {}
+ self.largs = []
+ self.rargs = rargs
+ self.order = []
+
+
+class OptionParser(object):
+ """The option parser is an internal class that is ultimately used to
+ parse options and arguments. It's modelled after optparse and brings
+ a similar but vastly simplified API. It should generally not be used
+ directly as the high level Click classes wrap it for you.
+
+ It's not nearly as extensible as optparse or argparse as it does not
+ implement features that are implemented on a higher level (such as
+ types or defaults).
+
+ :param ctx: optionally the :class:`~click.Context` where this parser
+ should go with.
+ """
+
+ def __init__(self, ctx=None):
+ #: The :class:`~click.Context` for this parser. This might be
+ #: `None` for some advanced use cases.
+ self.ctx = ctx
+ #: This controls how the parser deals with interspersed arguments.
+ #: If this is set to `False`, the parser will stop on the first
+ #: non-option. Click uses this to implement nested subcommands
+ #: safely.
+ self.allow_interspersed_args = True
+ #: This tells the parser how to deal with unknown options. By
+ #: default it will error out (which is sensible), but there is a
+ #: second mode where it will ignore it and continue processing
+ #: after shifting all the unknown options into the resulting args.
+ self.ignore_unknown_options = False
+ if ctx is not None:
+ self.allow_interspersed_args = ctx.allow_interspersed_args
+ self.ignore_unknown_options = ctx.ignore_unknown_options
+ self._short_opt = {}
+ self._long_opt = {}
+ self._opt_prefixes = {"-", "--"}
+ self._args = []
+
+ def add_option(self, opts, dest, action=None, nargs=1, const=None, obj=None):
+ """Adds a new option named `dest` to the parser. The destination
+ is not inferred (unlike with optparse) and needs to be explicitly
+ provided. Action can be any of ``store``, ``store_const``,
+ ``append``, ``appnd_const`` or ``count``.
+
+ The `obj` can be used to identify the option in the order list
+ that is returned from the parser.
+ """
+ if obj is None:
+ obj = dest
+ opts = [normalize_opt(opt, self.ctx) for opt in opts]
+ option = Option(opts, dest, action=action, nargs=nargs, const=const, obj=obj)
+ self._opt_prefixes.update(option.prefixes)
+ for opt in option._short_opts:
+ self._short_opt[opt] = option
+ for opt in option._long_opts:
+ self._long_opt[opt] = option
+
+ def add_argument(self, dest, nargs=1, obj=None):
+ """Adds a positional argument named `dest` to the parser.
+
+ The `obj` can be used to identify the option in the order list
+ that is returned from the parser.
+ """
+ if obj is None:
+ obj = dest
+ self._args.append(Argument(dest=dest, nargs=nargs, obj=obj))
+
+ def parse_args(self, args):
+ """Parses positional arguments and returns ``(values, args, order)``
+ for the parsed options and arguments as well as the leftover
+ arguments if there are any. The order is a list of objects as they
+ appear on the command line. If arguments appear multiple times they
+ will be memorized multiple times as well.
+ """
+ state = ParsingState(args)
+ try:
+ self._process_args_for_options(state)
+ self._process_args_for_args(state)
+ except UsageError:
+ if self.ctx is None or not self.ctx.resilient_parsing:
+ raise
+ return state.opts, state.largs, state.order
+
+ def _process_args_for_args(self, state):
+ pargs, args = _unpack_args(
+ state.largs + state.rargs, [x.nargs for x in self._args]
+ )
+
+ for idx, arg in enumerate(self._args):
+ arg.process(pargs[idx], state)
+
+ state.largs = args
+ state.rargs = []
+
+ def _process_args_for_options(self, state):
+ while state.rargs:
+ arg = state.rargs.pop(0)
+ arglen = len(arg)
+ # Double dashes always handled explicitly regardless of what
+ # prefixes are valid.
+ if arg == "--":
+ return
+ elif arg[:1] in self._opt_prefixes and arglen > 1:
+ self._process_opts(arg, state)
+ elif self.allow_interspersed_args:
+ state.largs.append(arg)
+ else:
+ state.rargs.insert(0, arg)
+ return
+
+ # Say this is the original argument list:
+ # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
+ # ^
+ # (we are about to process arg(i)).
+ #
+ # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of
+ # [arg0, ..., arg(i-1)] (any options and their arguments will have
+ # been removed from largs).
+ #
+ # The while loop will usually consume 1 or more arguments per pass.
+ # If it consumes 1 (eg. arg is an option that takes no arguments),
+ # then after _process_arg() is done the situation is:
+ #
+ # largs = subset of [arg0, ..., arg(i)]
+ # rargs = [arg(i+1), ..., arg(N-1)]
+ #
+ # If allow_interspersed_args is false, largs will always be
+ # *empty* -- still a subset of [arg0, ..., arg(i-1)], but
+ # not a very interesting subset!
+
+ def _match_long_opt(self, opt, explicit_value, state):
+ if opt not in self._long_opt:
+ possibilities = [word for word in self._long_opt if word.startswith(opt)]
+ raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
+
+ option = self._long_opt[opt]
+ if option.takes_value:
+ # At this point it's safe to modify rargs by injecting the
+ # explicit value, because no exception is raised in this
+ # branch. This means that the inserted value will be fully
+ # consumed.
+ if explicit_value is not None:
+ state.rargs.insert(0, explicit_value)
+
+ nargs = option.nargs
+ if len(state.rargs) < nargs:
+ _error_opt_args(nargs, opt)
+ elif nargs == 1:
+ value = state.rargs.pop(0)
+ else:
+ value = tuple(state.rargs[:nargs])
+ del state.rargs[:nargs]
+
+ elif explicit_value is not None:
+ raise BadOptionUsage(opt, "{} option does not take a value".format(opt))
+
+ else:
+ value = None
+
+ option.process(value, state)
+
+ def _match_short_opt(self, arg, state):
+ stop = False
+ i = 1
+ prefix = arg[0]
+ unknown_options = []
+
+ for ch in arg[1:]:
+ opt = normalize_opt(prefix + ch, self.ctx)
+ option = self._short_opt.get(opt)
+ i += 1
+
+ if not option:
+ if self.ignore_unknown_options:
+ unknown_options.append(ch)
+ continue
+ raise NoSuchOption(opt, ctx=self.ctx)
+ if option.takes_value:
+ # Any characters left in arg? Pretend they're the
+ # next arg, and stop consuming characters of arg.
+ if i < len(arg):
+ state.rargs.insert(0, arg[i:])
+ stop = True
+
+ nargs = option.nargs
+ if len(state.rargs) < nargs:
+ _error_opt_args(nargs, opt)
+ elif nargs == 1:
+ value = state.rargs.pop(0)
+ else:
+ value = tuple(state.rargs[:nargs])
+ del state.rargs[:nargs]
+
+ else:
+ value = None
+
+ option.process(value, state)
+
+ if stop:
+ break
+
+ # If we got any unknown options we re-combinate the string of the
+ # remaining options and re-attach the prefix, then report that
+ # to the state as new larg. This way there is basic combinatorics
+ # that can be achieved while still ignoring unknown arguments.
+ if self.ignore_unknown_options and unknown_options:
+ state.largs.append("{}{}".format(prefix, "".join(unknown_options)))
+
+ def _process_opts(self, arg, state):
+ explicit_value = None
+ # Long option handling happens in two parts. The first part is
+ # supporting explicitly attached values. In any case, we will try
+ # to long match the option first.
+ if "=" in arg:
+ long_opt, explicit_value = arg.split("=", 1)
+ else:
+ long_opt = arg
+ norm_long_opt = normalize_opt(long_opt, self.ctx)
+
+ # At this point we will match the (assumed) long option through
+ # the long option matching code. Note that this allows options
+ # like "-foo" to be matched as long options.
+ try:
+ self._match_long_opt(norm_long_opt, explicit_value, state)
+ except NoSuchOption:
+ # At this point the long option matching failed, and we need
+ # to try with short options. However there is a special rule
+ # which says, that if we have a two character options prefix
+ # (applies to "--foo" for instance), we do not dispatch to the
+ # short option code and will instead raise the no option
+ # error.
+ if arg[:2] not in self._opt_prefixes:
+ return self._match_short_opt(arg, state)
+ if not self.ignore_unknown_options:
+ raise
+ state.largs.append(arg)
diff --git a/mixly/tools/python/click/termui.py b/mixly/tools/python/click/termui.py
new file mode 100644
index 00000000..02ef9e9f
--- /dev/null
+++ b/mixly/tools/python/click/termui.py
@@ -0,0 +1,681 @@
+import inspect
+import io
+import itertools
+import os
+import struct
+import sys
+
+from ._compat import DEFAULT_COLUMNS
+from ._compat import get_winterm_size
+from ._compat import isatty
+from ._compat import raw_input
+from ._compat import string_types
+from ._compat import strip_ansi
+from ._compat import text_type
+from ._compat import WIN
+from .exceptions import Abort
+from .exceptions import UsageError
+from .globals import resolve_color_default
+from .types import Choice
+from .types import convert_type
+from .types import Path
+from .utils import echo
+from .utils import LazyFile
+
+# The prompt functions to use. The doc tools currently override these
+# functions to customize how they work.
+visible_prompt_func = raw_input
+
+_ansi_colors = {
+ "black": 30,
+ "red": 31,
+ "green": 32,
+ "yellow": 33,
+ "blue": 34,
+ "magenta": 35,
+ "cyan": 36,
+ "white": 37,
+ "reset": 39,
+ "bright_black": 90,
+ "bright_red": 91,
+ "bright_green": 92,
+ "bright_yellow": 93,
+ "bright_blue": 94,
+ "bright_magenta": 95,
+ "bright_cyan": 96,
+ "bright_white": 97,
+}
+_ansi_reset_all = "\033[0m"
+
+
+def hidden_prompt_func(prompt):
+ import getpass
+
+ return getpass.getpass(prompt)
+
+
+def _build_prompt(
+ text, suffix, show_default=False, default=None, show_choices=True, type=None
+):
+ prompt = text
+ if type is not None and show_choices and isinstance(type, Choice):
+ prompt += " ({})".format(", ".join(map(str, type.choices)))
+ if default is not None and show_default:
+ prompt = "{} [{}]".format(prompt, _format_default(default))
+ return prompt + suffix
+
+
+def _format_default(default):
+ if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"):
+ return default.name
+
+ return default
+
+
+def prompt(
+ text,
+ default=None,
+ hide_input=False,
+ confirmation_prompt=False,
+ type=None,
+ value_proc=None,
+ prompt_suffix=": ",
+ show_default=True,
+ err=False,
+ show_choices=True,
+):
+ """Prompts a user for input. This is a convenience function that can
+ be used to prompt a user for input later.
+
+ If the user aborts the input by sending a interrupt signal, this
+ function will catch it and raise a :exc:`Abort` exception.
+
+ .. versionadded:: 7.0
+ Added the show_choices parameter.
+
+ .. versionadded:: 6.0
+ Added unicode support for cmd.exe on Windows.
+
+ .. versionadded:: 4.0
+ Added the `err` parameter.
+
+ :param text: the text to show for the prompt.
+ :param default: the default value to use if no input happens. If this
+ is not given it will prompt until it's aborted.
+ :param hide_input: if this is set to true then the input value will
+ be hidden.
+ :param confirmation_prompt: asks for confirmation for the value.
+ :param type: the type to use to check the value against.
+ :param value_proc: if this parameter is provided it's a function that
+ is invoked instead of the type conversion to
+ convert a value.
+ :param prompt_suffix: a suffix that should be added to the prompt.
+ :param show_default: shows or hides the default value in the prompt.
+ :param err: if set to true the file defaults to ``stderr`` instead of
+ ``stdout``, the same as with echo.
+ :param show_choices: Show or hide choices if the passed type is a Choice.
+ For example if type is a Choice of either day or week,
+ show_choices is true and text is "Group by" then the
+ prompt will be "Group by (day, week): ".
+ """
+ result = None
+
+ def prompt_func(text):
+ f = hidden_prompt_func if hide_input else visible_prompt_func
+ try:
+ # Write the prompt separately so that we get nice
+ # coloring through colorama on Windows
+ echo(text, nl=False, err=err)
+ return f("")
+ except (KeyboardInterrupt, EOFError):
+ # getpass doesn't print a newline if the user aborts input with ^C.
+ # Allegedly this behavior is inherited from getpass(3).
+ # A doc bug has been filed at https://bugs.python.org/issue24711
+ if hide_input:
+ echo(None, err=err)
+ raise Abort()
+
+ if value_proc is None:
+ value_proc = convert_type(type, default)
+
+ prompt = _build_prompt(
+ text, prompt_suffix, show_default, default, show_choices, type
+ )
+
+ while 1:
+ while 1:
+ value = prompt_func(prompt)
+ if value:
+ break
+ elif default is not None:
+ if isinstance(value_proc, Path):
+ # validate Path default value(exists, dir_okay etc.)
+ value = default
+ break
+ return default
+ try:
+ result = value_proc(value)
+ except UsageError as e:
+ echo("Error: {}".format(e.message), err=err) # noqa: B306
+ continue
+ if not confirmation_prompt:
+ return result
+ while 1:
+ value2 = prompt_func("Repeat for confirmation: ")
+ if value2:
+ break
+ if value == value2:
+ return result
+ echo("Error: the two entered values do not match", err=err)
+
+
+def confirm(
+ text, default=False, abort=False, prompt_suffix=": ", show_default=True, err=False
+):
+ """Prompts for confirmation (yes/no question).
+
+ If the user aborts the input by sending a interrupt signal this
+ function will catch it and raise a :exc:`Abort` exception.
+
+ .. versionadded:: 4.0
+ Added the `err` parameter.
+
+ :param text: the question to ask.
+ :param default: the default for the prompt.
+ :param abort: if this is set to `True` a negative answer aborts the
+ exception by raising :exc:`Abort`.
+ :param prompt_suffix: a suffix that should be added to the prompt.
+ :param show_default: shows or hides the default value in the prompt.
+ :param err: if set to true the file defaults to ``stderr`` instead of
+ ``stdout``, the same as with echo.
+ """
+ prompt = _build_prompt(
+ text, prompt_suffix, show_default, "Y/n" if default else "y/N"
+ )
+ while 1:
+ try:
+ # Write the prompt separately so that we get nice
+ # coloring through colorama on Windows
+ echo(prompt, nl=False, err=err)
+ value = visible_prompt_func("").lower().strip()
+ except (KeyboardInterrupt, EOFError):
+ raise Abort()
+ if value in ("y", "yes"):
+ rv = True
+ elif value in ("n", "no"):
+ rv = False
+ elif value == "":
+ rv = default
+ else:
+ echo("Error: invalid input", err=err)
+ continue
+ break
+ if abort and not rv:
+ raise Abort()
+ return rv
+
+
+def get_terminal_size():
+ """Returns the current size of the terminal as tuple in the form
+ ``(width, height)`` in columns and rows.
+ """
+ # If shutil has get_terminal_size() (Python 3.3 and later) use that
+ if sys.version_info >= (3, 3):
+ import shutil
+
+ shutil_get_terminal_size = getattr(shutil, "get_terminal_size", None)
+ if shutil_get_terminal_size:
+ sz = shutil_get_terminal_size()
+ return sz.columns, sz.lines
+
+ # We provide a sensible default for get_winterm_size() when being invoked
+ # inside a subprocess. Without this, it would not provide a useful input.
+ if get_winterm_size is not None:
+ size = get_winterm_size()
+ if size == (0, 0):
+ return (79, 24)
+ else:
+ return size
+
+ def ioctl_gwinsz(fd):
+ try:
+ import fcntl
+ import termios
+
+ cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
+ except Exception:
+ return
+ return cr
+
+ cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2)
+ if not cr:
+ try:
+ fd = os.open(os.ctermid(), os.O_RDONLY)
+ try:
+ cr = ioctl_gwinsz(fd)
+ finally:
+ os.close(fd)
+ except Exception:
+ pass
+ if not cr or not cr[0] or not cr[1]:
+ cr = (os.environ.get("LINES", 25), os.environ.get("COLUMNS", DEFAULT_COLUMNS))
+ return int(cr[1]), int(cr[0])
+
+
+def echo_via_pager(text_or_generator, color=None):
+ """This function takes a text and shows it via an environment specific
+ pager on stdout.
+
+ .. versionchanged:: 3.0
+ Added the `color` flag.
+
+ :param text_or_generator: the text to page, or alternatively, a
+ generator emitting the text to page.
+ :param color: controls if the pager supports ANSI colors or not. The
+ default is autodetection.
+ """
+ color = resolve_color_default(color)
+
+ if inspect.isgeneratorfunction(text_or_generator):
+ i = text_or_generator()
+ elif isinstance(text_or_generator, string_types):
+ i = [text_or_generator]
+ else:
+ i = iter(text_or_generator)
+
+ # convert every element of i to a text type if necessary
+ text_generator = (el if isinstance(el, string_types) else text_type(el) for el in i)
+
+ from ._termui_impl import pager
+
+ return pager(itertools.chain(text_generator, "\n"), color)
+
+
+def progressbar(
+ iterable=None,
+ length=None,
+ label=None,
+ show_eta=True,
+ show_percent=None,
+ show_pos=False,
+ item_show_func=None,
+ fill_char="#",
+ empty_char="-",
+ bar_template="%(label)s [%(bar)s] %(info)s",
+ info_sep=" ",
+ width=36,
+ file=None,
+ color=None,
+):
+ """This function creates an iterable context manager that can be used
+ to iterate over something while showing a progress bar. It will
+ either iterate over the `iterable` or `length` items (that are counted
+ up). While iteration happens, this function will print a rendered
+ progress bar to the given `file` (defaults to stdout) and will attempt
+ to calculate remaining time and more. By default, this progress bar
+ will not be rendered if the file is not a terminal.
+
+ The context manager creates the progress bar. When the context
+ manager is entered the progress bar is already created. With every
+ iteration over the progress bar, the iterable passed to the bar is
+ advanced and the bar is updated. When the context manager exits,
+ a newline is printed and the progress bar is finalized on screen.
+
+ Note: The progress bar is currently designed for use cases where the
+ total progress can be expected to take at least several seconds.
+ Because of this, the ProgressBar class object won't display
+ progress that is considered too fast, and progress where the time
+ between steps is less than a second.
+
+ No printing must happen or the progress bar will be unintentionally
+ destroyed.
+
+ Example usage::
+
+ with progressbar(items) as bar:
+ for item in bar:
+ do_something_with(item)
+
+ Alternatively, if no iterable is specified, one can manually update the
+ progress bar through the `update()` method instead of directly
+ iterating over the progress bar. The update method accepts the number
+ of steps to increment the bar with::
+
+ with progressbar(length=chunks.total_bytes) as bar:
+ for chunk in chunks:
+ process_chunk(chunk)
+ bar.update(chunks.bytes)
+
+ .. versionadded:: 2.0
+
+ .. versionadded:: 4.0
+ Added the `color` parameter. Added a `update` method to the
+ progressbar object.
+
+ :param iterable: an iterable to iterate over. If not provided the length
+ is required.
+ :param length: the number of items to iterate over. By default the
+ progressbar will attempt to ask the iterator about its
+ length, which might or might not work. If an iterable is
+ also provided this parameter can be used to override the
+ length. If an iterable is not provided the progress bar
+ will iterate over a range of that length.
+ :param label: the label to show next to the progress bar.
+ :param show_eta: enables or disables the estimated time display. This is
+ automatically disabled if the length cannot be
+ determined.
+ :param show_percent: enables or disables the percentage display. The
+ default is `True` if the iterable has a length or
+ `False` if not.
+ :param show_pos: enables or disables the absolute position display. The
+ default is `False`.
+ :param item_show_func: a function called with the current item which
+ can return a string to show the current item
+ next to the progress bar. Note that the current
+ item can be `None`!
+ :param fill_char: the character to use to show the filled part of the
+ progress bar.
+ :param empty_char: the character to use to show the non-filled part of
+ the progress bar.
+ :param bar_template: the format string to use as template for the bar.
+ The parameters in it are ``label`` for the label,
+ ``bar`` for the progress bar and ``info`` for the
+ info section.
+ :param info_sep: the separator between multiple info items (eta etc.)
+ :param width: the width of the progress bar in characters, 0 means full
+ terminal width
+ :param file: the file to write to. If this is not a terminal then
+ only the label is printed.
+ :param color: controls if the terminal supports ANSI colors or not. The
+ default is autodetection. This is only needed if ANSI
+ codes are included anywhere in the progress bar output
+ which is not the case by default.
+ """
+ from ._termui_impl import ProgressBar
+
+ color = resolve_color_default(color)
+ return ProgressBar(
+ iterable=iterable,
+ length=length,
+ show_eta=show_eta,
+ show_percent=show_percent,
+ show_pos=show_pos,
+ item_show_func=item_show_func,
+ fill_char=fill_char,
+ empty_char=empty_char,
+ bar_template=bar_template,
+ info_sep=info_sep,
+ file=file,
+ label=label,
+ width=width,
+ color=color,
+ )
+
+
+def clear():
+ """Clears the terminal screen. This will have the effect of clearing
+ the whole visible space of the terminal and moving the cursor to the
+ top left. This does not do anything if not connected to a terminal.
+
+ .. versionadded:: 2.0
+ """
+ if not isatty(sys.stdout):
+ return
+ # If we're on Windows and we don't have colorama available, then we
+ # clear the screen by shelling out. Otherwise we can use an escape
+ # sequence.
+ if WIN:
+ os.system("cls")
+ else:
+ sys.stdout.write("\033[2J\033[1;1H")
+
+
+def style(
+ text,
+ fg=None,
+ bg=None,
+ bold=None,
+ dim=None,
+ underline=None,
+ blink=None,
+ reverse=None,
+ reset=True,
+):
+ """Styles a text with ANSI styles and returns the new string. By
+ default the styling is self contained which means that at the end
+ of the string a reset code is issued. This can be prevented by
+ passing ``reset=False``.
+
+ Examples::
+
+ click.echo(click.style('Hello World!', fg='green'))
+ click.echo(click.style('ATTENTION!', blink=True))
+ click.echo(click.style('Some things', reverse=True, fg='cyan'))
+
+ Supported color names:
+
+ * ``black`` (might be a gray)
+ * ``red``
+ * ``green``
+ * ``yellow`` (might be an orange)
+ * ``blue``
+ * ``magenta``
+ * ``cyan``
+ * ``white`` (might be light gray)
+ * ``bright_black``
+ * ``bright_red``
+ * ``bright_green``
+ * ``bright_yellow``
+ * ``bright_blue``
+ * ``bright_magenta``
+ * ``bright_cyan``
+ * ``bright_white``
+ * ``reset`` (reset the color code only)
+
+ .. versionadded:: 2.0
+
+ .. versionadded:: 7.0
+ Added support for bright colors.
+
+ :param text: the string to style with ansi codes.
+ :param fg: if provided this will become the foreground color.
+ :param bg: if provided this will become the background color.
+ :param bold: if provided this will enable or disable bold mode.
+ :param dim: if provided this will enable or disable dim mode. This is
+ badly supported.
+ :param underline: if provided this will enable or disable underline.
+ :param blink: if provided this will enable or disable blinking.
+ :param reverse: if provided this will enable or disable inverse
+ rendering (foreground becomes background and the
+ other way round).
+ :param reset: by default a reset-all code is added at the end of the
+ string which means that styles do not carry over. This
+ can be disabled to compose styles.
+ """
+ bits = []
+ if fg:
+ try:
+ bits.append("\033[{}m".format(_ansi_colors[fg]))
+ except KeyError:
+ raise TypeError("Unknown color '{}'".format(fg))
+ if bg:
+ try:
+ bits.append("\033[{}m".format(_ansi_colors[bg] + 10))
+ except KeyError:
+ raise TypeError("Unknown color '{}'".format(bg))
+ if bold is not None:
+ bits.append("\033[{}m".format(1 if bold else 22))
+ if dim is not None:
+ bits.append("\033[{}m".format(2 if dim else 22))
+ if underline is not None:
+ bits.append("\033[{}m".format(4 if underline else 24))
+ if blink is not None:
+ bits.append("\033[{}m".format(5 if blink else 25))
+ if reverse is not None:
+ bits.append("\033[{}m".format(7 if reverse else 27))
+ bits.append(text)
+ if reset:
+ bits.append(_ansi_reset_all)
+ return "".join(bits)
+
+
+def unstyle(text):
+ """Removes ANSI styling information from a string. Usually it's not
+ necessary to use this function as Click's echo function will
+ automatically remove styling if necessary.
+
+ .. versionadded:: 2.0
+
+ :param text: the text to remove style information from.
+ """
+ return strip_ansi(text)
+
+
+def secho(message=None, file=None, nl=True, err=False, color=None, **styles):
+ """This function combines :func:`echo` and :func:`style` into one
+ call. As such the following two calls are the same::
+
+ click.secho('Hello World!', fg='green')
+ click.echo(click.style('Hello World!', fg='green'))
+
+ All keyword arguments are forwarded to the underlying functions
+ depending on which one they go with.
+
+ .. versionadded:: 2.0
+ """
+ if message is not None:
+ message = style(message, **styles)
+ return echo(message, file=file, nl=nl, err=err, color=color)
+
+
+def edit(
+ text=None, editor=None, env=None, require_save=True, extension=".txt", filename=None
+):
+ r"""Edits the given text in the defined editor. If an editor is given
+ (should be the full path to the executable but the regular operating
+ system search path is used for finding the executable) it overrides
+ the detected editor. Optionally, some environment variables can be
+ used. If the editor is closed without changes, `None` is returned. In
+ case a file is edited directly the return value is always `None` and
+ `require_save` and `extension` are ignored.
+
+ If the editor cannot be opened a :exc:`UsageError` is raised.
+
+ Note for Windows: to simplify cross-platform usage, the newlines are
+ automatically converted from POSIX to Windows and vice versa. As such,
+ the message here will have ``\n`` as newline markers.
+
+ :param text: the text to edit.
+ :param editor: optionally the editor to use. Defaults to automatic
+ detection.
+ :param env: environment variables to forward to the editor.
+ :param require_save: if this is true, then not saving in the editor
+ will make the return value become `None`.
+ :param extension: the extension to tell the editor about. This defaults
+ to `.txt` but changing this might change syntax
+ highlighting.
+ :param filename: if provided it will edit this file instead of the
+ provided text contents. It will not use a temporary
+ file as an indirection in that case.
+ """
+ from ._termui_impl import Editor
+
+ editor = Editor(
+ editor=editor, env=env, require_save=require_save, extension=extension
+ )
+ if filename is None:
+ return editor.edit(text)
+ editor.edit_file(filename)
+
+
+def launch(url, wait=False, locate=False):
+ """This function launches the given URL (or filename) in the default
+ viewer application for this file type. If this is an executable, it
+ might launch the executable in a new session. The return value is
+ the exit code of the launched application. Usually, ``0`` indicates
+ success.
+
+ Examples::
+
+ click.launch('https://click.palletsprojects.com/')
+ click.launch('/my/downloaded/file', locate=True)
+
+ .. versionadded:: 2.0
+
+ :param url: URL or filename of the thing to launch.
+ :param wait: waits for the program to stop.
+ :param locate: if this is set to `True` then instead of launching the
+ application associated with the URL it will attempt to
+ launch a file manager with the file located. This
+ might have weird effects if the URL does not point to
+ the filesystem.
+ """
+ from ._termui_impl import open_url
+
+ return open_url(url, wait=wait, locate=locate)
+
+
+# If this is provided, getchar() calls into this instead. This is used
+# for unittesting purposes.
+_getchar = None
+
+
+def getchar(echo=False):
+ """Fetches a single character from the terminal and returns it. This
+ will always return a unicode character and under certain rare
+ circumstances this might return more than one character. The
+ situations which more than one character is returned is when for
+ whatever reason multiple characters end up in the terminal buffer or
+ standard input was not actually a terminal.
+
+ Note that this will always read from the terminal, even if something
+ is piped into the standard input.
+
+ Note for Windows: in rare cases when typing non-ASCII characters, this
+ function might wait for a second character and then return both at once.
+ This is because certain Unicode characters look like special-key markers.
+
+ .. versionadded:: 2.0
+
+ :param echo: if set to `True`, the character read will also show up on
+ the terminal. The default is to not show it.
+ """
+ f = _getchar
+ if f is None:
+ from ._termui_impl import getchar as f
+ return f(echo)
+
+
+def raw_terminal():
+ from ._termui_impl import raw_terminal as f
+
+ return f()
+
+
+def pause(info="Press any key to continue ...", err=False):
+ """This command stops execution and waits for the user to press any
+ key to continue. This is similar to the Windows batch "pause"
+ command. If the program is not run through a terminal, this command
+ will instead do nothing.
+
+ .. versionadded:: 2.0
+
+ .. versionadded:: 4.0
+ Added the `err` parameter.
+
+ :param info: the info string to print before pausing.
+ :param err: if set to message goes to ``stderr`` instead of
+ ``stdout``, the same as with echo.
+ """
+ if not isatty(sys.stdin) or not isatty(sys.stdout):
+ return
+ try:
+ if info:
+ echo(info, nl=False, err=err)
+ try:
+ getchar()
+ except (KeyboardInterrupt, EOFError):
+ pass
+ finally:
+ if info:
+ echo(err=err)
diff --git a/mixly/tools/python/click/testing.py b/mixly/tools/python/click/testing.py
new file mode 100644
index 00000000..a3dba3b3
--- /dev/null
+++ b/mixly/tools/python/click/testing.py
@@ -0,0 +1,382 @@
+import contextlib
+import os
+import shlex
+import shutil
+import sys
+import tempfile
+
+from . import formatting
+from . import termui
+from . import utils
+from ._compat import iteritems
+from ._compat import PY2
+from ._compat import string_types
+
+
+if PY2:
+ from cStringIO import StringIO
+else:
+ import io
+ from ._compat import _find_binary_reader
+
+
+class EchoingStdin(object):
+ def __init__(self, input, output):
+ self._input = input
+ self._output = output
+
+ def __getattr__(self, x):
+ return getattr(self._input, x)
+
+ def _echo(self, rv):
+ self._output.write(rv)
+ return rv
+
+ def read(self, n=-1):
+ return self._echo(self._input.read(n))
+
+ def readline(self, n=-1):
+ return self._echo(self._input.readline(n))
+
+ def readlines(self):
+ return [self._echo(x) for x in self._input.readlines()]
+
+ def __iter__(self):
+ return iter(self._echo(x) for x in self._input)
+
+ def __repr__(self):
+ return repr(self._input)
+
+
+def make_input_stream(input, charset):
+ # Is already an input stream.
+ if hasattr(input, "read"):
+ if PY2:
+ return input
+ rv = _find_binary_reader(input)
+ if rv is not None:
+ return rv
+ raise TypeError("Could not find binary reader for input stream.")
+
+ if input is None:
+ input = b""
+ elif not isinstance(input, bytes):
+ input = input.encode(charset)
+ if PY2:
+ return StringIO(input)
+ return io.BytesIO(input)
+
+
+class Result(object):
+ """Holds the captured result of an invoked CLI script."""
+
+ def __init__(
+ self, runner, stdout_bytes, stderr_bytes, exit_code, exception, exc_info=None
+ ):
+ #: The runner that created the result
+ self.runner = runner
+ #: The standard output as bytes.
+ self.stdout_bytes = stdout_bytes
+ #: The standard error as bytes, or None if not available
+ self.stderr_bytes = stderr_bytes
+ #: The exit code as integer.
+ self.exit_code = exit_code
+ #: The exception that happened if one did.
+ self.exception = exception
+ #: The traceback
+ self.exc_info = exc_info
+
+ @property
+ def output(self):
+ """The (standard) output as unicode string."""
+ return self.stdout
+
+ @property
+ def stdout(self):
+ """The standard output as unicode string."""
+ return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
+ "\r\n", "\n"
+ )
+
+ @property
+ def stderr(self):
+ """The standard error as unicode string."""
+ if self.stderr_bytes is None:
+ raise ValueError("stderr not separately captured")
+ return self.stderr_bytes.decode(self.runner.charset, "replace").replace(
+ "\r\n", "\n"
+ )
+
+ def __repr__(self):
+ return "<{} {}>".format(
+ type(self).__name__, repr(self.exception) if self.exception else "okay"
+ )
+
+
+class CliRunner(object):
+ """The CLI runner provides functionality to invoke a Click command line
+ script for unittesting purposes in a isolated environment. This only
+ works in single-threaded systems without any concurrency as it changes the
+ global interpreter state.
+
+ :param charset: the character set for the input and output data. This is
+ UTF-8 by default and should not be changed currently as
+ the reporting to Click only works in Python 2 properly.
+ :param env: a dictionary with environment variables for overriding.
+ :param echo_stdin: if this is set to `True`, then reading from stdin writes
+ to stdout. This is useful for showing examples in
+ some circumstances. Note that regular prompts
+ will automatically echo the input.
+ :param mix_stderr: if this is set to `False`, then stdout and stderr are
+ preserved as independent streams. This is useful for
+ Unix-philosophy apps that have predictable stdout and
+ noisy stderr, such that each may be measured
+ independently
+ """
+
+ def __init__(self, charset=None, env=None, echo_stdin=False, mix_stderr=True):
+ if charset is None:
+ charset = "utf-8"
+ self.charset = charset
+ self.env = env or {}
+ self.echo_stdin = echo_stdin
+ self.mix_stderr = mix_stderr
+
+ def get_default_prog_name(self, cli):
+ """Given a command object it will return the default program name
+ for it. The default is the `name` attribute or ``"root"`` if not
+ set.
+ """
+ return cli.name or "root"
+
+ def make_env(self, overrides=None):
+ """Returns the environment overrides for invoking a script."""
+ rv = dict(self.env)
+ if overrides:
+ rv.update(overrides)
+ return rv
+
+ @contextlib.contextmanager
+ def isolation(self, input=None, env=None, color=False):
+ """A context manager that sets up the isolation for invoking of a
+ command line tool. This sets up stdin with the given input data
+ and `os.environ` with the overrides from the given dictionary.
+ This also rebinds some internals in Click to be mocked (like the
+ prompt functionality).
+
+ This is automatically done in the :meth:`invoke` method.
+
+ .. versionadded:: 4.0
+ The ``color`` parameter was added.
+
+ :param input: the input stream to put into sys.stdin.
+ :param env: the environment overrides as dictionary.
+ :param color: whether the output should contain color codes. The
+ application can still override this explicitly.
+ """
+ input = make_input_stream(input, self.charset)
+
+ old_stdin = sys.stdin
+ old_stdout = sys.stdout
+ old_stderr = sys.stderr
+ old_forced_width = formatting.FORCED_WIDTH
+ formatting.FORCED_WIDTH = 80
+
+ env = self.make_env(env)
+
+ if PY2:
+ bytes_output = StringIO()
+ if self.echo_stdin:
+ input = EchoingStdin(input, bytes_output)
+ sys.stdout = bytes_output
+ if not self.mix_stderr:
+ bytes_error = StringIO()
+ sys.stderr = bytes_error
+ else:
+ bytes_output = io.BytesIO()
+ if self.echo_stdin:
+ input = EchoingStdin(input, bytes_output)
+ input = io.TextIOWrapper(input, encoding=self.charset)
+ sys.stdout = io.TextIOWrapper(bytes_output, encoding=self.charset)
+ if not self.mix_stderr:
+ bytes_error = io.BytesIO()
+ sys.stderr = io.TextIOWrapper(bytes_error, encoding=self.charset)
+
+ if self.mix_stderr:
+ sys.stderr = sys.stdout
+
+ sys.stdin = input
+
+ def visible_input(prompt=None):
+ sys.stdout.write(prompt or "")
+ val = input.readline().rstrip("\r\n")
+ sys.stdout.write("{}\n".format(val))
+ sys.stdout.flush()
+ return val
+
+ def hidden_input(prompt=None):
+ sys.stdout.write("{}\n".format(prompt or ""))
+ sys.stdout.flush()
+ return input.readline().rstrip("\r\n")
+
+ def _getchar(echo):
+ char = sys.stdin.read(1)
+ if echo:
+ sys.stdout.write(char)
+ sys.stdout.flush()
+ return char
+
+ default_color = color
+
+ def should_strip_ansi(stream=None, color=None):
+ if color is None:
+ return not default_color
+ return not color
+
+ old_visible_prompt_func = termui.visible_prompt_func
+ old_hidden_prompt_func = termui.hidden_prompt_func
+ old__getchar_func = termui._getchar
+ old_should_strip_ansi = utils.should_strip_ansi
+ termui.visible_prompt_func = visible_input
+ termui.hidden_prompt_func = hidden_input
+ termui._getchar = _getchar
+ utils.should_strip_ansi = should_strip_ansi
+
+ old_env = {}
+ try:
+ for key, value in iteritems(env):
+ old_env[key] = os.environ.get(key)
+ if value is None:
+ try:
+ del os.environ[key]
+ except Exception:
+ pass
+ else:
+ os.environ[key] = value
+ yield (bytes_output, not self.mix_stderr and bytes_error)
+ finally:
+ for key, value in iteritems(old_env):
+ if value is None:
+ try:
+ del os.environ[key]
+ except Exception:
+ pass
+ else:
+ os.environ[key] = value
+ sys.stdout = old_stdout
+ sys.stderr = old_stderr
+ sys.stdin = old_stdin
+ termui.visible_prompt_func = old_visible_prompt_func
+ termui.hidden_prompt_func = old_hidden_prompt_func
+ termui._getchar = old__getchar_func
+ utils.should_strip_ansi = old_should_strip_ansi
+ formatting.FORCED_WIDTH = old_forced_width
+
+ def invoke(
+ self,
+ cli,
+ args=None,
+ input=None,
+ env=None,
+ catch_exceptions=True,
+ color=False,
+ **extra
+ ):
+ """Invokes a command in an isolated environment. The arguments are
+ forwarded directly to the command line script, the `extra` keyword
+ arguments are passed to the :meth:`~clickpkg.Command.main` function of
+ the command.
+
+ This returns a :class:`Result` object.
+
+ .. versionadded:: 3.0
+ The ``catch_exceptions`` parameter was added.
+
+ .. versionchanged:: 3.0
+ The result object now has an `exc_info` attribute with the
+ traceback if available.
+
+ .. versionadded:: 4.0
+ The ``color`` parameter was added.
+
+ :param cli: the command to invoke
+ :param args: the arguments to invoke. It may be given as an iterable
+ or a string. When given as string it will be interpreted
+ as a Unix shell command. More details at
+ :func:`shlex.split`.
+ :param input: the input data for `sys.stdin`.
+ :param env: the environment overrides.
+ :param catch_exceptions: Whether to catch any other exceptions than
+ ``SystemExit``.
+ :param extra: the keyword arguments to pass to :meth:`main`.
+ :param color: whether the output should contain color codes. The
+ application can still override this explicitly.
+ """
+ exc_info = None
+ with self.isolation(input=input, env=env, color=color) as outstreams:
+ exception = None
+ exit_code = 0
+
+ if isinstance(args, string_types):
+ args = shlex.split(args)
+
+ try:
+ prog_name = extra.pop("prog_name")
+ except KeyError:
+ prog_name = self.get_default_prog_name(cli)
+
+ try:
+ cli.main(args=args or (), prog_name=prog_name, **extra)
+ except SystemExit as e:
+ exc_info = sys.exc_info()
+ exit_code = e.code
+ if exit_code is None:
+ exit_code = 0
+
+ if exit_code != 0:
+ exception = e
+
+ if not isinstance(exit_code, int):
+ sys.stdout.write(str(exit_code))
+ sys.stdout.write("\n")
+ exit_code = 1
+
+ except Exception as e:
+ if not catch_exceptions:
+ raise
+ exception = e
+ exit_code = 1
+ exc_info = sys.exc_info()
+ finally:
+ sys.stdout.flush()
+ stdout = outstreams[0].getvalue()
+ if self.mix_stderr:
+ stderr = None
+ else:
+ stderr = outstreams[1].getvalue()
+
+ return Result(
+ runner=self,
+ stdout_bytes=stdout,
+ stderr_bytes=stderr,
+ exit_code=exit_code,
+ exception=exception,
+ exc_info=exc_info,
+ )
+
+ @contextlib.contextmanager
+ def isolated_filesystem(self):
+ """A context manager that creates a temporary folder and changes
+ the current working directory to it for isolated filesystem tests.
+ """
+ cwd = os.getcwd()
+ t = tempfile.mkdtemp()
+ os.chdir(t)
+ try:
+ yield t
+ finally:
+ os.chdir(cwd)
+ try:
+ shutil.rmtree(t)
+ except (OSError, IOError): # noqa: B014
+ pass
diff --git a/mixly/tools/python/click/types.py b/mixly/tools/python/click/types.py
new file mode 100644
index 00000000..505c39f8
--- /dev/null
+++ b/mixly/tools/python/click/types.py
@@ -0,0 +1,762 @@
+import os
+import stat
+from datetime import datetime
+
+from ._compat import _get_argv_encoding
+from ._compat import filename_to_ui
+from ._compat import get_filesystem_encoding
+from ._compat import get_streerror
+from ._compat import open_stream
+from ._compat import PY2
+from ._compat import text_type
+from .exceptions import BadParameter
+from .utils import LazyFile
+from .utils import safecall
+
+
+class ParamType(object):
+ """Helper for converting values through types. The following is
+ necessary for a valid type:
+
+ * it needs a name
+ * it needs to pass through None unchanged
+ * it needs to convert from a string
+ * it needs to convert its result type through unchanged
+ (eg: needs to be idempotent)
+ * it needs to be able to deal with param and context being `None`.
+ This can be the case when the object is used with prompt
+ inputs.
+ """
+
+ is_composite = False
+
+ #: the descriptive name of this type
+ name = None
+
+ #: if a list of this type is expected and the value is pulled from a
+ #: string environment variable, this is what splits it up. `None`
+ #: means any whitespace. For all parameters the general rule is that
+ #: whitespace splits them up. The exception are paths and files which
+ #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
+ #: Windows).
+ envvar_list_splitter = None
+
+ def __call__(self, value, param=None, ctx=None):
+ if value is not None:
+ return self.convert(value, param, ctx)
+
+ def get_metavar(self, param):
+ """Returns the metavar default for this param if it provides one."""
+
+ def get_missing_message(self, param):
+ """Optionally might return extra information about a missing
+ parameter.
+
+ .. versionadded:: 2.0
+ """
+
+ def convert(self, value, param, ctx):
+ """Converts the value. This is not invoked for values that are
+ `None` (the missing value).
+ """
+ return value
+
+ def split_envvar_value(self, rv):
+ """Given a value from an environment variable this splits it up
+ into small chunks depending on the defined envvar list splitter.
+
+ If the splitter is set to `None`, which means that whitespace splits,
+ then leading and trailing whitespace is ignored. Otherwise, leading
+ and trailing splitters usually lead to empty items being included.
+ """
+ return (rv or "").split(self.envvar_list_splitter)
+
+ def fail(self, message, param=None, ctx=None):
+ """Helper method to fail with an invalid value message."""
+ raise BadParameter(message, ctx=ctx, param=param)
+
+
+class CompositeParamType(ParamType):
+ is_composite = True
+
+ @property
+ def arity(self):
+ raise NotImplementedError()
+
+
+class FuncParamType(ParamType):
+ def __init__(self, func):
+ self.name = func.__name__
+ self.func = func
+
+ def convert(self, value, param, ctx):
+ try:
+ return self.func(value)
+ except ValueError:
+ try:
+ value = text_type(value)
+ except UnicodeError:
+ value = str(value).decode("utf-8", "replace")
+ self.fail(value, param, ctx)
+
+
+class UnprocessedParamType(ParamType):
+ name = "text"
+
+ def convert(self, value, param, ctx):
+ return value
+
+ def __repr__(self):
+ return "UNPROCESSED"
+
+
+class StringParamType(ParamType):
+ name = "text"
+
+ def convert(self, value, param, ctx):
+ if isinstance(value, bytes):
+ enc = _get_argv_encoding()
+ try:
+ value = value.decode(enc)
+ except UnicodeError:
+ fs_enc = get_filesystem_encoding()
+ if fs_enc != enc:
+ try:
+ value = value.decode(fs_enc)
+ except UnicodeError:
+ value = value.decode("utf-8", "replace")
+ else:
+ value = value.decode("utf-8", "replace")
+ return value
+ return value
+
+ def __repr__(self):
+ return "STRING"
+
+
+class Choice(ParamType):
+ """The choice type allows a value to be checked against a fixed set
+ of supported values. All of these values have to be strings.
+
+ You should only pass a list or tuple of choices. Other iterables
+ (like generators) may lead to surprising results.
+
+ The resulting value will always be one of the originally passed choices
+ regardless of ``case_sensitive`` or any ``ctx.token_normalize_func``
+ being specified.
+
+ See :ref:`choice-opts` for an example.
+
+ :param case_sensitive: Set to false to make choices case
+ insensitive. Defaults to true.
+ """
+
+ name = "choice"
+
+ def __init__(self, choices, case_sensitive=True):
+ self.choices = choices
+ self.case_sensitive = case_sensitive
+
+ def get_metavar(self, param):
+ return "[{}]".format("|".join(self.choices))
+
+ def get_missing_message(self, param):
+ return "Choose from:\n\t{}.".format(",\n\t".join(self.choices))
+
+ def convert(self, value, param, ctx):
+ # Match through normalization and case sensitivity
+ # first do token_normalize_func, then lowercase
+ # preserve original `value` to produce an accurate message in
+ # `self.fail`
+ normed_value = value
+ normed_choices = {choice: choice for choice in self.choices}
+
+ if ctx is not None and ctx.token_normalize_func is not None:
+ normed_value = ctx.token_normalize_func(value)
+ normed_choices = {
+ ctx.token_normalize_func(normed_choice): original
+ for normed_choice, original in normed_choices.items()
+ }
+
+ if not self.case_sensitive:
+ if PY2:
+ lower = str.lower
+ else:
+ lower = str.casefold
+
+ normed_value = lower(normed_value)
+ normed_choices = {
+ lower(normed_choice): original
+ for normed_choice, original in normed_choices.items()
+ }
+
+ if normed_value in normed_choices:
+ return normed_choices[normed_value]
+
+ self.fail(
+ "invalid choice: {}. (choose from {})".format(
+ value, ", ".join(self.choices)
+ ),
+ param,
+ ctx,
+ )
+
+ def __repr__(self):
+ return "Choice('{}')".format(list(self.choices))
+
+
+class DateTime(ParamType):
+ """The DateTime type converts date strings into `datetime` objects.
+
+ The format strings which are checked are configurable, but default to some
+ common (non-timezone aware) ISO 8601 formats.
+
+ When specifying *DateTime* formats, you should only pass a list or a tuple.
+ Other iterables, like generators, may lead to surprising results.
+
+ The format strings are processed using ``datetime.strptime``, and this
+ consequently defines the format strings which are allowed.
+
+ Parsing is tried using each format, in order, and the first format which
+ parses successfully is used.
+
+ :param formats: A list or tuple of date format strings, in the order in
+ which they should be tried. Defaults to
+ ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``,
+ ``'%Y-%m-%d %H:%M:%S'``.
+ """
+
+ name = "datetime"
+
+ def __init__(self, formats=None):
+ self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
+
+ def get_metavar(self, param):
+ return "[{}]".format("|".join(self.formats))
+
+ def _try_to_convert_date(self, value, format):
+ try:
+ return datetime.strptime(value, format)
+ except ValueError:
+ return None
+
+ def convert(self, value, param, ctx):
+ # Exact match
+ for format in self.formats:
+ dtime = self._try_to_convert_date(value, format)
+ if dtime:
+ return dtime
+
+ self.fail(
+ "invalid datetime format: {}. (choose from {})".format(
+ value, ", ".join(self.formats)
+ )
+ )
+
+ def __repr__(self):
+ return "DateTime"
+
+
+class IntParamType(ParamType):
+ name = "integer"
+
+ def convert(self, value, param, ctx):
+ try:
+ return int(value)
+ except ValueError:
+ self.fail("{} is not a valid integer".format(value), param, ctx)
+
+ def __repr__(self):
+ return "INT"
+
+
+class IntRange(IntParamType):
+ """A parameter that works similar to :data:`click.INT` but restricts
+ the value to fit into a range. The default behavior is to fail if the
+ value falls outside the range, but it can also be silently clamped
+ between the two edges.
+
+ See :ref:`ranges` for an example.
+ """
+
+ name = "integer range"
+
+ def __init__(self, min=None, max=None, clamp=False):
+ self.min = min
+ self.max = max
+ self.clamp = clamp
+
+ def convert(self, value, param, ctx):
+ rv = IntParamType.convert(self, value, param, ctx)
+ if self.clamp:
+ if self.min is not None and rv < self.min:
+ return self.min
+ if self.max is not None and rv > self.max:
+ return self.max
+ if (
+ self.min is not None
+ and rv < self.min
+ or self.max is not None
+ and rv > self.max
+ ):
+ if self.min is None:
+ self.fail(
+ "{} is bigger than the maximum valid value {}.".format(
+ rv, self.max
+ ),
+ param,
+ ctx,
+ )
+ elif self.max is None:
+ self.fail(
+ "{} is smaller than the minimum valid value {}.".format(
+ rv, self.min
+ ),
+ param,
+ ctx,
+ )
+ else:
+ self.fail(
+ "{} is not in the valid range of {} to {}.".format(
+ rv, self.min, self.max
+ ),
+ param,
+ ctx,
+ )
+ return rv
+
+ def __repr__(self):
+ return "IntRange({}, {})".format(self.min, self.max)
+
+
+class FloatParamType(ParamType):
+ name = "float"
+
+ def convert(self, value, param, ctx):
+ try:
+ return float(value)
+ except ValueError:
+ self.fail(
+ "{} is not a valid floating point value".format(value), param, ctx
+ )
+
+ def __repr__(self):
+ return "FLOAT"
+
+
+class FloatRange(FloatParamType):
+ """A parameter that works similar to :data:`click.FLOAT` but restricts
+ the value to fit into a range. The default behavior is to fail if the
+ value falls outside the range, but it can also be silently clamped
+ between the two edges.
+
+ See :ref:`ranges` for an example.
+ """
+
+ name = "float range"
+
+ def __init__(self, min=None, max=None, clamp=False):
+ self.min = min
+ self.max = max
+ self.clamp = clamp
+
+ def convert(self, value, param, ctx):
+ rv = FloatParamType.convert(self, value, param, ctx)
+ if self.clamp:
+ if self.min is not None and rv < self.min:
+ return self.min
+ if self.max is not None and rv > self.max:
+ return self.max
+ if (
+ self.min is not None
+ and rv < self.min
+ or self.max is not None
+ and rv > self.max
+ ):
+ if self.min is None:
+ self.fail(
+ "{} is bigger than the maximum valid value {}.".format(
+ rv, self.max
+ ),
+ param,
+ ctx,
+ )
+ elif self.max is None:
+ self.fail(
+ "{} is smaller than the minimum valid value {}.".format(
+ rv, self.min
+ ),
+ param,
+ ctx,
+ )
+ else:
+ self.fail(
+ "{} is not in the valid range of {} to {}.".format(
+ rv, self.min, self.max
+ ),
+ param,
+ ctx,
+ )
+ return rv
+
+ def __repr__(self):
+ return "FloatRange({}, {})".format(self.min, self.max)
+
+
+class BoolParamType(ParamType):
+ name = "boolean"
+
+ def convert(self, value, param, ctx):
+ if isinstance(value, bool):
+ return bool(value)
+ value = value.lower()
+ if value in ("true", "t", "1", "yes", "y"):
+ return True
+ elif value in ("false", "f", "0", "no", "n"):
+ return False
+ self.fail("{} is not a valid boolean".format(value), param, ctx)
+
+ def __repr__(self):
+ return "BOOL"
+
+
+class UUIDParameterType(ParamType):
+ name = "uuid"
+
+ def convert(self, value, param, ctx):
+ import uuid
+
+ try:
+ if PY2 and isinstance(value, text_type):
+ value = value.encode("ascii")
+ return uuid.UUID(value)
+ except ValueError:
+ self.fail("{} is not a valid UUID value".format(value), param, ctx)
+
+ def __repr__(self):
+ return "UUID"
+
+
+class File(ParamType):
+ """Declares a parameter to be a file for reading or writing. The file
+ is automatically closed once the context tears down (after the command
+ finished working).
+
+ Files can be opened for reading or writing. The special value ``-``
+ indicates stdin or stdout depending on the mode.
+
+ By default, the file is opened for reading text data, but it can also be
+ opened in binary mode or for writing. The encoding parameter can be used
+ to force a specific encoding.
+
+ The `lazy` flag controls if the file should be opened immediately or upon
+ first IO. The default is to be non-lazy for standard input and output
+ streams as well as files opened for reading, `lazy` otherwise. When opening a
+ file lazily for reading, it is still opened temporarily for validation, but
+ will not be held open until first IO. lazy is mainly useful when opening
+ for writing to avoid creating the file until it is needed.
+
+ Starting with Click 2.0, files can also be opened atomically in which
+ case all writes go into a separate file in the same folder and upon
+ completion the file will be moved over to the original location. This
+ is useful if a file regularly read by other users is modified.
+
+ See :ref:`file-args` for more information.
+ """
+
+ name = "filename"
+ envvar_list_splitter = os.path.pathsep
+
+ def __init__(
+ self, mode="r", encoding=None, errors="strict", lazy=None, atomic=False
+ ):
+ self.mode = mode
+ self.encoding = encoding
+ self.errors = errors
+ self.lazy = lazy
+ self.atomic = atomic
+
+ def resolve_lazy_flag(self, value):
+ if self.lazy is not None:
+ return self.lazy
+ if value == "-":
+ return False
+ elif "w" in self.mode:
+ return True
+ return False
+
+ def convert(self, value, param, ctx):
+ try:
+ if hasattr(value, "read") or hasattr(value, "write"):
+ return value
+
+ lazy = self.resolve_lazy_flag(value)
+
+ if lazy:
+ f = LazyFile(
+ value, self.mode, self.encoding, self.errors, atomic=self.atomic
+ )
+ if ctx is not None:
+ ctx.call_on_close(f.close_intelligently)
+ return f
+
+ f, should_close = open_stream(
+ value, self.mode, self.encoding, self.errors, atomic=self.atomic
+ )
+ # If a context is provided, we automatically close the file
+ # at the end of the context execution (or flush out). If a
+ # context does not exist, it's the caller's responsibility to
+ # properly close the file. This for instance happens when the
+ # type is used with prompts.
+ if ctx is not None:
+ if should_close:
+ ctx.call_on_close(safecall(f.close))
+ else:
+ ctx.call_on_close(safecall(f.flush))
+ return f
+ except (IOError, OSError) as e: # noqa: B014
+ self.fail(
+ "Could not open file: {}: {}".format(
+ filename_to_ui(value), get_streerror(e)
+ ),
+ param,
+ ctx,
+ )
+
+
+class Path(ParamType):
+ """The path type is similar to the :class:`File` type but it performs
+ different checks. First of all, instead of returning an open file
+ handle it returns just the filename. Secondly, it can perform various
+ basic checks about what the file or directory should be.
+
+ .. versionchanged:: 6.0
+ `allow_dash` was added.
+
+ :param exists: if set to true, the file or directory needs to exist for
+ this value to be valid. If this is not required and a
+ file does indeed not exist, then all further checks are
+ silently skipped.
+ :param file_okay: controls if a file is a possible value.
+ :param dir_okay: controls if a directory is a possible value.
+ :param writable: if true, a writable check is performed.
+ :param readable: if true, a readable check is performed.
+ :param resolve_path: if this is true, then the path is fully resolved
+ before the value is passed onwards. This means
+ that it's absolute and symlinks are resolved. It
+ will not expand a tilde-prefix, as this is
+ supposed to be done by the shell only.
+ :param allow_dash: If this is set to `True`, a single dash to indicate
+ standard streams is permitted.
+ :param path_type: optionally a string type that should be used to
+ represent the path. The default is `None` which
+ means the return value will be either bytes or
+ unicode depending on what makes most sense given the
+ input data Click deals with.
+ """
+
+ envvar_list_splitter = os.path.pathsep
+
+ def __init__(
+ self,
+ exists=False,
+ file_okay=True,
+ dir_okay=True,
+ writable=False,
+ readable=True,
+ resolve_path=False,
+ allow_dash=False,
+ path_type=None,
+ ):
+ self.exists = exists
+ self.file_okay = file_okay
+ self.dir_okay = dir_okay
+ self.writable = writable
+ self.readable = readable
+ self.resolve_path = resolve_path
+ self.allow_dash = allow_dash
+ self.type = path_type
+
+ if self.file_okay and not self.dir_okay:
+ self.name = "file"
+ self.path_type = "File"
+ elif self.dir_okay and not self.file_okay:
+ self.name = "directory"
+ self.path_type = "Directory"
+ else:
+ self.name = "path"
+ self.path_type = "Path"
+
+ def coerce_path_result(self, rv):
+ if self.type is not None and not isinstance(rv, self.type):
+ if self.type is text_type:
+ rv = rv.decode(get_filesystem_encoding())
+ else:
+ rv = rv.encode(get_filesystem_encoding())
+ return rv
+
+ def convert(self, value, param, ctx):
+ rv = value
+
+ is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
+
+ if not is_dash:
+ if self.resolve_path:
+ rv = os.path.realpath(rv)
+
+ try:
+ st = os.stat(rv)
+ except OSError:
+ if not self.exists:
+ return self.coerce_path_result(rv)
+ self.fail(
+ "{} '{}' does not exist.".format(
+ self.path_type, filename_to_ui(value)
+ ),
+ param,
+ ctx,
+ )
+
+ if not self.file_okay and stat.S_ISREG(st.st_mode):
+ self.fail(
+ "{} '{}' is a file.".format(self.path_type, filename_to_ui(value)),
+ param,
+ ctx,
+ )
+ if not self.dir_okay and stat.S_ISDIR(st.st_mode):
+ self.fail(
+ "{} '{}' is a directory.".format(
+ self.path_type, filename_to_ui(value)
+ ),
+ param,
+ ctx,
+ )
+ if self.writable and not os.access(value, os.W_OK):
+ self.fail(
+ "{} '{}' is not writable.".format(
+ self.path_type, filename_to_ui(value)
+ ),
+ param,
+ ctx,
+ )
+ if self.readable and not os.access(value, os.R_OK):
+ self.fail(
+ "{} '{}' is not readable.".format(
+ self.path_type, filename_to_ui(value)
+ ),
+ param,
+ ctx,
+ )
+
+ return self.coerce_path_result(rv)
+
+
+class Tuple(CompositeParamType):
+ """The default behavior of Click is to apply a type on a value directly.
+ This works well in most cases, except for when `nargs` is set to a fixed
+ count and different types should be used for different items. In this
+ case the :class:`Tuple` type can be used. This type can only be used
+ if `nargs` is set to a fixed number.
+
+ For more information see :ref:`tuple-type`.
+
+ This can be selected by using a Python tuple literal as a type.
+
+ :param types: a list of types that should be used for the tuple items.
+ """
+
+ def __init__(self, types):
+ self.types = [convert_type(ty) for ty in types]
+
+ @property
+ def name(self):
+ return "<{}>".format(" ".join(ty.name for ty in self.types))
+
+ @property
+ def arity(self):
+ return len(self.types)
+
+ def convert(self, value, param, ctx):
+ if len(value) != len(self.types):
+ raise TypeError(
+ "It would appear that nargs is set to conflict with the"
+ " composite type arity."
+ )
+ return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value))
+
+
+def convert_type(ty, default=None):
+ """Converts a callable or python type into the most appropriate
+ param type.
+ """
+ guessed_type = False
+ if ty is None and default is not None:
+ if isinstance(default, tuple):
+ ty = tuple(map(type, default))
+ else:
+ ty = type(default)
+ guessed_type = True
+
+ if isinstance(ty, tuple):
+ return Tuple(ty)
+ if isinstance(ty, ParamType):
+ return ty
+ if ty is text_type or ty is str or ty is None:
+ return STRING
+ if ty is int:
+ return INT
+ # Booleans are only okay if not guessed. This is done because for
+ # flags the default value is actually a bit of a lie in that it
+ # indicates which of the flags is the one we want. See get_default()
+ # for more information.
+ if ty is bool and not guessed_type:
+ return BOOL
+ if ty is float:
+ return FLOAT
+ if guessed_type:
+ return STRING
+
+ # Catch a common mistake
+ if __debug__:
+ try:
+ if issubclass(ty, ParamType):
+ raise AssertionError(
+ "Attempted to use an uninstantiated parameter type ({}).".format(ty)
+ )
+ except TypeError:
+ pass
+ return FuncParamType(ty)
+
+
+#: A dummy parameter type that just does nothing. From a user's
+#: perspective this appears to just be the same as `STRING` but internally
+#: no string conversion takes place. This is necessary to achieve the
+#: same bytes/unicode behavior on Python 2/3 in situations where you want
+#: to not convert argument types. This is usually useful when working
+#: with file paths as they can appear in bytes and unicode.
+#:
+#: For path related uses the :class:`Path` type is a better choice but
+#: there are situations where an unprocessed type is useful which is why
+#: it is is provided.
+#:
+#: .. versionadded:: 4.0
+UNPROCESSED = UnprocessedParamType()
+
+#: A unicode string parameter type which is the implicit default. This
+#: can also be selected by using ``str`` as type.
+STRING = StringParamType()
+
+#: An integer parameter. This can also be selected by using ``int`` as
+#: type.
+INT = IntParamType()
+
+#: A floating point value parameter. This can also be selected by using
+#: ``float`` as type.
+FLOAT = FloatParamType()
+
+#: A boolean parameter. This is the default for boolean flags. This can
+#: also be selected by using ``bool`` as a type.
+BOOL = BoolParamType()
+
+#: A UUID parameter.
+UUID = UUIDParameterType()
diff --git a/mixly/tools/python/click/utils.py b/mixly/tools/python/click/utils.py
new file mode 100644
index 00000000..79265e73
--- /dev/null
+++ b/mixly/tools/python/click/utils.py
@@ -0,0 +1,455 @@
+import os
+import sys
+
+from ._compat import _default_text_stderr
+from ._compat import _default_text_stdout
+from ._compat import auto_wrap_for_ansi
+from ._compat import binary_streams
+from ._compat import filename_to_ui
+from ._compat import get_filesystem_encoding
+from ._compat import get_streerror
+from ._compat import is_bytes
+from ._compat import open_stream
+from ._compat import PY2
+from ._compat import should_strip_ansi
+from ._compat import string_types
+from ._compat import strip_ansi
+from ._compat import text_streams
+from ._compat import text_type
+from ._compat import WIN
+from .globals import resolve_color_default
+
+if not PY2:
+ from ._compat import _find_binary_writer
+elif WIN:
+ from ._winconsole import _get_windows_argv
+ from ._winconsole import _hash_py_argv
+ from ._winconsole import _initial_argv_hash
+
+echo_native_types = string_types + (bytes, bytearray)
+
+
+def _posixify(name):
+ return "-".join(name.split()).lower()
+
+
+def safecall(func):
+ """Wraps a function so that it swallows exceptions."""
+
+ def wrapper(*args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except Exception:
+ pass
+
+ return wrapper
+
+
+def make_str(value):
+ """Converts a value into a valid string."""
+ if isinstance(value, bytes):
+ try:
+ return value.decode(get_filesystem_encoding())
+ except UnicodeError:
+ return value.decode("utf-8", "replace")
+ return text_type(value)
+
+
+def make_default_short_help(help, max_length=45):
+ """Return a condensed version of help string."""
+ words = help.split()
+ total_length = 0
+ result = []
+ done = False
+
+ for word in words:
+ if word[-1:] == ".":
+ done = True
+ new_length = 1 + len(word) if result else len(word)
+ if total_length + new_length > max_length:
+ result.append("...")
+ done = True
+ else:
+ if result:
+ result.append(" ")
+ result.append(word)
+ if done:
+ break
+ total_length += new_length
+
+ return "".join(result)
+
+
+class LazyFile(object):
+ """A lazy file works like a regular file but it does not fully open
+ the file but it does perform some basic checks early to see if the
+ filename parameter does make sense. This is useful for safely opening
+ files for writing.
+ """
+
+ def __init__(
+ self, filename, mode="r", encoding=None, errors="strict", atomic=False
+ ):
+ self.name = filename
+ self.mode = mode
+ self.encoding = encoding
+ self.errors = errors
+ self.atomic = atomic
+
+ if filename == "-":
+ self._f, self.should_close = open_stream(filename, mode, encoding, errors)
+ else:
+ if "r" in mode:
+ # Open and close the file in case we're opening it for
+ # reading so that we can catch at least some errors in
+ # some cases early.
+ open(filename, mode).close()
+ self._f = None
+ self.should_close = True
+
+ def __getattr__(self, name):
+ return getattr(self.open(), name)
+
+ def __repr__(self):
+ if self._f is not None:
+ return repr(self._f)
+ return "".format(self.name, self.mode)
+
+ def open(self):
+ """Opens the file if it's not yet open. This call might fail with
+ a :exc:`FileError`. Not handling this error will produce an error
+ that Click shows.
+ """
+ if self._f is not None:
+ return self._f
+ try:
+ rv, self.should_close = open_stream(
+ self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
+ )
+ except (IOError, OSError) as e: # noqa: E402
+ from .exceptions import FileError
+
+ raise FileError(self.name, hint=get_streerror(e))
+ self._f = rv
+ return rv
+
+ def close(self):
+ """Closes the underlying file, no matter what."""
+ if self._f is not None:
+ self._f.close()
+
+ def close_intelligently(self):
+ """This function only closes the file if it was opened by the lazy
+ file wrapper. For instance this will never close stdin.
+ """
+ if self.should_close:
+ self.close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb):
+ self.close_intelligently()
+
+ def __iter__(self):
+ self.open()
+ return iter(self._f)
+
+
+class KeepOpenFile(object):
+ def __init__(self, file):
+ self._file = file
+
+ def __getattr__(self, name):
+ return getattr(self._file, name)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb):
+ pass
+
+ def __repr__(self):
+ return repr(self._file)
+
+ def __iter__(self):
+ return iter(self._file)
+
+
+def echo(message=None, file=None, nl=True, err=False, color=None):
+ """Prints a message plus a newline to the given file or stdout. On
+ first sight, this looks like the print function, but it has improved
+ support for handling Unicode and binary data that does not fail no
+ matter how badly configured the system is.
+
+ Primarily it means that you can print binary data as well as Unicode
+ data on both 2.x and 3.x to the given file in the most appropriate way
+ possible. This is a very carefree function in that it will try its
+ best to not fail. As of Click 6.0 this includes support for unicode
+ output on the Windows console.
+
+ In addition to that, if `colorama`_ is installed, the echo function will
+ also support clever handling of ANSI codes. Essentially it will then
+ do the following:
+
+ - add transparent handling of ANSI color codes on Windows.
+ - hide ANSI codes automatically if the destination file is not a
+ terminal.
+
+ .. _colorama: https://pypi.org/project/colorama/
+
+ .. versionchanged:: 6.0
+ As of Click 6.0 the echo function will properly support unicode
+ output on the windows console. Not that click does not modify
+ the interpreter in any way which means that `sys.stdout` or the
+ print statement or function will still not provide unicode support.
+
+ .. versionchanged:: 2.0
+ Starting with version 2.0 of Click, the echo function will work
+ with colorama if it's installed.
+
+ .. versionadded:: 3.0
+ The `err` parameter was added.
+
+ .. versionchanged:: 4.0
+ Added the `color` flag.
+
+ :param message: the message to print
+ :param file: the file to write to (defaults to ``stdout``)
+ :param err: if set to true the file defaults to ``stderr`` instead of
+ ``stdout``. This is faster and easier than calling
+ :func:`get_text_stderr` yourself.
+ :param nl: if set to `True` (the default) a newline is printed afterwards.
+ :param color: controls if the terminal supports ANSI colors or not. The
+ default is autodetection.
+ """
+ if file is None:
+ if err:
+ file = _default_text_stderr()
+ else:
+ file = _default_text_stdout()
+
+ # Convert non bytes/text into the native string type.
+ if message is not None and not isinstance(message, echo_native_types):
+ message = text_type(message)
+
+ if nl:
+ message = message or u""
+ if isinstance(message, text_type):
+ message += u"\n"
+ else:
+ message += b"\n"
+
+ # If there is a message, and we're in Python 3, and the value looks
+ # like bytes, we manually need to find the binary stream and write the
+ # message in there. This is done separately so that most stream
+ # types will work as you would expect. Eg: you can write to StringIO
+ # for other cases.
+ if message and not PY2 and is_bytes(message):
+ binary_file = _find_binary_writer(file)
+ if binary_file is not None:
+ file.flush()
+ binary_file.write(message)
+ binary_file.flush()
+ return
+
+ # ANSI-style support. If there is no message or we are dealing with
+ # bytes nothing is happening. If we are connected to a file we want
+ # to strip colors. If we are on windows we either wrap the stream
+ # to strip the color or we use the colorama support to translate the
+ # ansi codes to API calls.
+ if message and not is_bytes(message):
+ color = resolve_color_default(color)
+ if should_strip_ansi(file, color):
+ message = strip_ansi(message)
+ elif WIN:
+ if auto_wrap_for_ansi is not None:
+ file = auto_wrap_for_ansi(file)
+ elif not color:
+ message = strip_ansi(message)
+
+ if message:
+ file.write(message)
+ file.flush()
+
+
+def get_binary_stream(name):
+ """Returns a system stream for byte processing. This essentially
+ returns the stream from the sys module with the given name but it
+ solves some compatibility issues between different Python versions.
+ Primarily this function is necessary for getting binary streams on
+ Python 3.
+
+ :param name: the name of the stream to open. Valid names are ``'stdin'``,
+ ``'stdout'`` and ``'stderr'``
+ """
+ opener = binary_streams.get(name)
+ if opener is None:
+ raise TypeError("Unknown standard stream '{}'".format(name))
+ return opener()
+
+
+def get_text_stream(name, encoding=None, errors="strict"):
+ """Returns a system stream for text processing. This usually returns
+ a wrapped stream around a binary stream returned from
+ :func:`get_binary_stream` but it also can take shortcuts on Python 3
+ for already correctly configured streams.
+
+ :param name: the name of the stream to open. Valid names are ``'stdin'``,
+ ``'stdout'`` and ``'stderr'``
+ :param encoding: overrides the detected default encoding.
+ :param errors: overrides the default error mode.
+ """
+ opener = text_streams.get(name)
+ if opener is None:
+ raise TypeError("Unknown standard stream '{}'".format(name))
+ return opener(encoding, errors)
+
+
+def open_file(
+ filename, mode="r", encoding=None, errors="strict", lazy=False, atomic=False
+):
+ """This is similar to how the :class:`File` works but for manual
+ usage. Files are opened non lazy by default. This can open regular
+ files as well as stdin/stdout if ``'-'`` is passed.
+
+ If stdin/stdout is returned the stream is wrapped so that the context
+ manager will not close the stream accidentally. This makes it possible
+ to always use the function like this without having to worry to
+ accidentally close a standard stream::
+
+ with open_file(filename) as f:
+ ...
+
+ .. versionadded:: 3.0
+
+ :param filename: the name of the file to open (or ``'-'`` for stdin/stdout).
+ :param mode: the mode in which to open the file.
+ :param encoding: the encoding to use.
+ :param errors: the error handling for this file.
+ :param lazy: can be flipped to true to open the file lazily.
+ :param atomic: in atomic mode writes go into a temporary file and it's
+ moved on close.
+ """
+ if lazy:
+ return LazyFile(filename, mode, encoding, errors, atomic=atomic)
+ f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
+ if not should_close:
+ f = KeepOpenFile(f)
+ return f
+
+
+def get_os_args():
+ """This returns the argument part of sys.argv in the most appropriate
+ form for processing. What this means is that this return value is in
+ a format that works for Click to process but does not necessarily
+ correspond well to what's actually standard for the interpreter.
+
+ On most environments the return value is ``sys.argv[:1]`` unchanged.
+ However if you are on Windows and running Python 2 the return value
+ will actually be a list of unicode strings instead because the
+ default behavior on that platform otherwise will not be able to
+ carry all possible values that sys.argv can have.
+
+ .. versionadded:: 6.0
+ """
+ # We can only extract the unicode argv if sys.argv has not been
+ # changed since the startup of the application.
+ if PY2 and WIN and _initial_argv_hash == _hash_py_argv():
+ return _get_windows_argv()
+ return sys.argv[1:]
+
+
+def format_filename(filename, shorten=False):
+ """Formats a filename for user display. The main purpose of this
+ function is to ensure that the filename can be displayed at all. This
+ will decode the filename to unicode if necessary in a way that it will
+ not fail. Optionally, it can shorten the filename to not include the
+ full path to the filename.
+
+ :param filename: formats a filename for UI display. This will also convert
+ the filename into unicode without failing.
+ :param shorten: this optionally shortens the filename to strip of the
+ path that leads up to it.
+ """
+ if shorten:
+ filename = os.path.basename(filename)
+ return filename_to_ui(filename)
+
+
+def get_app_dir(app_name, roaming=True, force_posix=False):
+ r"""Returns the config folder for the application. The default behavior
+ is to return whatever is most appropriate for the operating system.
+
+ To give you an idea, for an app called ``"Foo Bar"``, something like
+ the following folders could be returned:
+
+ Mac OS X:
+ ``~/Library/Application Support/Foo Bar``
+ Mac OS X (POSIX):
+ ``~/.foo-bar``
+ Unix:
+ ``~/.config/foo-bar``
+ Unix (POSIX):
+ ``~/.foo-bar``
+ Win XP (roaming):
+ ``C:\Documents and Settings\\Local Settings\Application Data\Foo Bar``
+ Win XP (not roaming):
+ ``C:\Documents and Settings\\Application Data\Foo Bar``
+ Win 7 (roaming):
+ ``C:\Users\\AppData\Roaming\Foo Bar``
+ Win 7 (not roaming):
+ ``C:\Users\\AppData\Local\Foo Bar``
+
+ .. versionadded:: 2.0
+
+ :param app_name: the application name. This should be properly capitalized
+ and can contain whitespace.
+ :param roaming: controls if the folder should be roaming or not on Windows.
+ Has no affect otherwise.
+ :param force_posix: if this is set to `True` then on any POSIX system the
+ folder will be stored in the home folder with a leading
+ dot instead of the XDG config home or darwin's
+ application support folder.
+ """
+ if WIN:
+ key = "APPDATA" if roaming else "LOCALAPPDATA"
+ folder = os.environ.get(key)
+ if folder is None:
+ folder = os.path.expanduser("~")
+ return os.path.join(folder, app_name)
+ if force_posix:
+ return os.path.join(os.path.expanduser("~/.{}".format(_posixify(app_name))))
+ if sys.platform == "darwin":
+ return os.path.join(
+ os.path.expanduser("~/Library/Application Support"), app_name
+ )
+ return os.path.join(
+ os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
+ _posixify(app_name),
+ )
+
+
+class PacifyFlushWrapper(object):
+ """This wrapper is used to catch and suppress BrokenPipeErrors resulting
+ from ``.flush()`` being called on broken pipe during the shutdown/final-GC
+ of the Python interpreter. Notably ``.flush()`` is always called on
+ ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any
+ other cleanup code, and the case where the underlying file is not a broken
+ pipe, all calls and attributes are proxied.
+ """
+
+ def __init__(self, wrapped):
+ self.wrapped = wrapped
+
+ def flush(self):
+ try:
+ self.wrapped.flush()
+ except IOError as e:
+ import errno
+
+ if e.errno != errno.EPIPE:
+ raise
+
+ def __getattr__(self, attr):
+ return getattr(self.wrapped, attr)
diff --git a/mixly/tools/python/dotenv/__init__.py b/mixly/tools/python/dotenv/__init__.py
new file mode 100644
index 00000000..b88d9bc2
--- /dev/null
+++ b/mixly/tools/python/dotenv/__init__.py
@@ -0,0 +1,46 @@
+from .compat import IS_TYPE_CHECKING
+from .main import load_dotenv, get_key, set_key, unset_key, find_dotenv, dotenv_values
+
+if IS_TYPE_CHECKING:
+ from typing import Any, Optional
+
+
+def load_ipython_extension(ipython):
+ # type: (Any) -> None
+ from .ipython import load_ipython_extension
+ load_ipython_extension(ipython)
+
+
+def get_cli_string(path=None, action=None, key=None, value=None, quote=None):
+ # type: (Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]) -> str
+ """Returns a string suitable for running as a shell script.
+
+ Useful for converting a arguments passed to a fabric task
+ to be passed to a `local` or `run` command.
+ """
+ command = ['dotenv']
+ if quote:
+ command.append('-q %s' % quote)
+ if path:
+ command.append('-f %s' % path)
+ if action:
+ command.append(action)
+ if key:
+ command.append(key)
+ if value:
+ if ' ' in value:
+ command.append('"%s"' % value)
+ else:
+ command.append(value)
+
+ return ' '.join(command).strip()
+
+
+__all__ = ['get_cli_string',
+ 'load_dotenv',
+ 'dotenv_values',
+ 'get_key',
+ 'set_key',
+ 'unset_key',
+ 'find_dotenv',
+ 'load_ipython_extension']
diff --git a/mixly/tools/python/dotenv/cli.py b/mixly/tools/python/dotenv/cli.py
new file mode 100644
index 00000000..bb96c023
--- /dev/null
+++ b/mixly/tools/python/dotenv/cli.py
@@ -0,0 +1,174 @@
+import os
+import sys
+from subprocess import Popen
+
+try:
+ import click
+except ImportError:
+ sys.stderr.write('It seems python-dotenv is not installed with cli option. \n'
+ 'Run pip install "python-dotenv[cli]" to fix this.')
+ sys.exit(1)
+
+from .compat import IS_TYPE_CHECKING, to_env
+from .main import dotenv_values, get_key, set_key, unset_key
+from .version import __version__
+
+if IS_TYPE_CHECKING:
+ from typing import Any, List, Dict
+
+
+@click.group()
+@click.option('-f', '--file', default=os.path.join(os.getcwd(), '.env'),
+ type=click.Path(file_okay=True),
+ help="Location of the .env file, defaults to .env file in current working directory.")
+@click.option('-q', '--quote', default='always',
+ type=click.Choice(['always', 'never', 'auto']),
+ help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.")
+@click.option('-e', '--export', default=False,
+ type=click.BOOL,
+ help="Whether to write the dot file as an executable bash script.")
+@click.version_option(version=__version__)
+@click.pass_context
+def cli(ctx, file, quote, export):
+ # type: (click.Context, Any, Any, Any) -> None
+ '''This script is used to set, get or unset values from a .env file.'''
+ ctx.obj = {}
+ ctx.obj['QUOTE'] = quote
+ ctx.obj['EXPORT'] = export
+ ctx.obj['FILE'] = file
+
+
+@cli.command()
+@click.pass_context
+def list(ctx):
+ # type: (click.Context) -> None
+ '''Display all the stored key/value.'''
+ file = ctx.obj['FILE']
+ if not os.path.isfile(file):
+ raise click.BadParameter(
+ 'Path "%s" does not exist.' % (file),
+ ctx=ctx
+ )
+ dotenv_as_dict = dotenv_values(file)
+ for k, v in dotenv_as_dict.items():
+ click.echo('%s=%s' % (k, v))
+
+
+@cli.command()
+@click.pass_context
+@click.argument('key', required=True)
+@click.argument('value', required=True)
+def set(ctx, key, value):
+ # type: (click.Context, Any, Any) -> None
+ '''Store the given key/value.'''
+ file = ctx.obj['FILE']
+ quote = ctx.obj['QUOTE']
+ export = ctx.obj['EXPORT']
+ success, key, value = set_key(file, key, value, quote, export)
+ if success:
+ click.echo('%s=%s' % (key, value))
+ else:
+ exit(1)
+
+
+@cli.command()
+@click.pass_context
+@click.argument('key', required=True)
+def get(ctx, key):
+ # type: (click.Context, Any) -> None
+ '''Retrieve the value for the given key.'''
+ file = ctx.obj['FILE']
+ if not os.path.isfile(file):
+ raise click.BadParameter(
+ 'Path "%s" does not exist.' % (file),
+ ctx=ctx
+ )
+ stored_value = get_key(file, key)
+ if stored_value:
+ click.echo(stored_value)
+ else:
+ exit(1)
+
+
+@cli.command()
+@click.pass_context
+@click.argument('key', required=True)
+def unset(ctx, key):
+ # type: (click.Context, Any) -> None
+ '''Removes the given key.'''
+ file = ctx.obj['FILE']
+ quote = ctx.obj['QUOTE']
+ success, key = unset_key(file, key, quote)
+ if success:
+ click.echo("Successfully removed %s" % key)
+ else:
+ exit(1)
+
+
+@cli.command(context_settings={'ignore_unknown_options': True})
+@click.pass_context
+@click.option(
+ "--override/--no-override",
+ default=True,
+ help="Override variables from the environment file with those from the .env file.",
+)
+@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
+def run(ctx, override, commandline):
+ # type: (click.Context, bool, List[str]) -> None
+ """Run command with environment variables present."""
+ file = ctx.obj['FILE']
+ if not os.path.isfile(file):
+ raise click.BadParameter(
+ 'Invalid value for \'-f\' "%s" does not exist.' % (file),
+ ctx=ctx
+ )
+ dotenv_as_dict = {
+ to_env(k): to_env(v)
+ for (k, v) in dotenv_values(file).items()
+ if v is not None and (override or to_env(k) not in os.environ)
+ }
+
+ if not commandline:
+ click.echo('No command given.')
+ exit(1)
+ ret = run_command(commandline, dotenv_as_dict)
+ exit(ret)
+
+
+def run_command(command, env):
+ # type: (List[str], Dict[str, str]) -> int
+ """Run command in sub process.
+
+ Runs the command in a sub process with the variables from `env`
+ added in the current environment variables.
+
+ Parameters
+ ----------
+ command: List[str]
+ The command and it's parameters
+ env: Dict
+ The additional environment variables
+
+ Returns
+ -------
+ int
+ The return code of the command
+
+ """
+ # copy the current environment variables and add the vales from
+ # `env`
+ cmd_env = os.environ.copy()
+ cmd_env.update(env)
+
+ p = Popen(command,
+ universal_newlines=True,
+ bufsize=0,
+ shell=False,
+ env=cmd_env)
+ _, _ = p.communicate()
+
+ return p.returncode
+
+
+if __name__ == "__main__":
+ cli()
diff --git a/mixly/tools/python/dotenv/compat.py b/mixly/tools/python/dotenv/compat.py
new file mode 100644
index 00000000..f8089bf4
--- /dev/null
+++ b/mixly/tools/python/dotenv/compat.py
@@ -0,0 +1,49 @@
+import sys
+
+PY2 = sys.version_info[0] == 2 # type: bool
+
+if PY2:
+ from StringIO import StringIO # noqa
+else:
+ from io import StringIO # noqa
+
+
+def is_type_checking():
+ # type: () -> bool
+ try:
+ from typing import TYPE_CHECKING
+ except ImportError:
+ return False
+ return TYPE_CHECKING
+
+
+IS_TYPE_CHECKING = is_type_checking()
+
+
+if IS_TYPE_CHECKING:
+ from typing import Text
+
+
+def to_env(text):
+ # type: (Text) -> str
+ """
+ Encode a string the same way whether it comes from the environment or a `.env` file.
+ """
+ if PY2:
+ return text.encode(sys.getfilesystemencoding() or "utf-8")
+ else:
+ return text
+
+
+def to_text(string):
+ # type: (str) -> Text
+ """
+ Make a string Unicode if it isn't already.
+
+ This is useful for defining raw unicode strings because `ur"foo"` isn't valid in
+ Python 3.
+ """
+ if PY2:
+ return string.decode("utf-8")
+ else:
+ return string
diff --git a/mixly/tools/python/dotenv/ipython.py b/mixly/tools/python/dotenv/ipython.py
new file mode 100644
index 00000000..7f1b13d6
--- /dev/null
+++ b/mixly/tools/python/dotenv/ipython.py
@@ -0,0 +1,41 @@
+from __future__ import print_function
+
+from IPython.core.magic import Magics, line_magic, magics_class # type: ignore
+from IPython.core.magic_arguments import (argument, magic_arguments, # type: ignore
+ parse_argstring) # type: ignore
+
+from .main import find_dotenv, load_dotenv
+
+
+@magics_class
+class IPythonDotEnv(Magics):
+
+ @magic_arguments()
+ @argument(
+ '-o', '--override', action='store_true',
+ help="Indicate to override existing variables"
+ )
+ @argument(
+ '-v', '--verbose', action='store_true',
+ help="Indicate function calls to be verbose"
+ )
+ @argument('dotenv_path', nargs='?', type=str, default='.env',
+ help='Search in increasingly higher folders for the `dotenv_path`')
+ @line_magic
+ def dotenv(self, line):
+ args = parse_argstring(self.dotenv, line)
+ # Locate the .env file
+ dotenv_path = args.dotenv_path
+ try:
+ dotenv_path = find_dotenv(dotenv_path, True, True)
+ except IOError:
+ print("cannot find .env file")
+ return
+
+ # Load the .env file
+ load_dotenv(dotenv_path, verbose=args.verbose, override=args.override)
+
+
+def load_ipython_extension(ipython):
+ """Register the %dotenv magic."""
+ ipython.register_magics(IPythonDotEnv)
diff --git a/mixly/tools/python/dotenv/main.py b/mixly/tools/python/dotenv/main.py
new file mode 100644
index 00000000..16f22d2c
--- /dev/null
+++ b/mixly/tools/python/dotenv/main.py
@@ -0,0 +1,355 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, print_function, unicode_literals
+
+import io
+import logging
+import os
+import shutil
+import sys
+import tempfile
+from collections import OrderedDict
+from contextlib import contextmanager
+
+from .compat import IS_TYPE_CHECKING, PY2, StringIO, to_env
+from .parser import Binding, parse_stream
+from .variables import parse_variables
+
+logger = logging.getLogger(__name__)
+
+if IS_TYPE_CHECKING:
+ from typing import (IO, Dict, Iterable, Iterator, Mapping, Optional, Text,
+ Tuple, Union)
+ if sys.version_info >= (3, 6):
+ _PathLike = os.PathLike
+ else:
+ _PathLike = Text
+
+ if sys.version_info >= (3, 0):
+ _StringIO = StringIO
+ else:
+ _StringIO = StringIO[Text]
+
+
+def with_warn_for_invalid_lines(mappings):
+ # type: (Iterator[Binding]) -> Iterator[Binding]
+ for mapping in mappings:
+ if mapping.error:
+ logger.warning(
+ "Python-dotenv could not parse statement starting at line %s",
+ mapping.original.line,
+ )
+ yield mapping
+
+
+class DotEnv():
+
+ def __init__(self, dotenv_path, verbose=False, encoding=None, interpolate=True, override=True):
+ # type: (Union[Text, _PathLike, _StringIO], bool, Union[None, Text], bool, bool) -> None
+ self.dotenv_path = dotenv_path # type: Union[Text,_PathLike, _StringIO]
+ self._dict = None # type: Optional[Dict[Text, Optional[Text]]]
+ self.verbose = verbose # type: bool
+ self.encoding = encoding # type: Union[None, Text]
+ self.interpolate = interpolate # type: bool
+ self.override = override # type: bool
+
+ @contextmanager
+ def _get_stream(self):
+ # type: () -> Iterator[IO[Text]]
+ if isinstance(self.dotenv_path, StringIO):
+ yield self.dotenv_path
+ elif os.path.isfile(self.dotenv_path):
+ with io.open(self.dotenv_path, encoding=self.encoding) as stream:
+ yield stream
+ else:
+ if self.verbose:
+ logger.info("Python-dotenv could not find configuration file %s.", self.dotenv_path or '.env')
+ yield StringIO('')
+
+ def dict(self):
+ # type: () -> Dict[Text, Optional[Text]]
+ """Return dotenv as dict"""
+ if self._dict:
+ return self._dict
+
+ raw_values = self.parse()
+
+ if self.interpolate:
+ self._dict = OrderedDict(resolve_variables(raw_values, override=self.override))
+ else:
+ self._dict = OrderedDict(raw_values)
+
+ return self._dict
+
+ def parse(self):
+ # type: () -> Iterator[Tuple[Text, Optional[Text]]]
+ with self._get_stream() as stream:
+ for mapping in with_warn_for_invalid_lines(parse_stream(stream)):
+ if mapping.key is not None:
+ yield mapping.key, mapping.value
+
+ def set_as_environment_variables(self):
+ # type: () -> bool
+ """
+ Load the current dotenv as system environment variable.
+ """
+ for k, v in self.dict().items():
+ if k in os.environ and not self.override:
+ continue
+ if v is not None:
+ os.environ[to_env(k)] = to_env(v)
+
+ return True
+
+ def get(self, key):
+ # type: (Text) -> Optional[Text]
+ """
+ """
+ data = self.dict()
+
+ if key in data:
+ return data[key]
+
+ if self.verbose:
+ logger.warning("Key %s not found in %s.", key, self.dotenv_path)
+
+ return None
+
+
+def get_key(dotenv_path, key_to_get):
+ # type: (Union[Text, _PathLike], Text) -> Optional[Text]
+ """
+ Gets the value of a given key from the given .env
+
+ If the .env path given doesn't exist, fails
+ """
+ return DotEnv(dotenv_path, verbose=True).get(key_to_get)
+
+
+@contextmanager
+def rewrite(path):
+ # type: (_PathLike) -> Iterator[Tuple[IO[Text], IO[Text]]]
+ try:
+ if not os.path.isfile(path):
+ with io.open(path, "w+") as source:
+ source.write("")
+ with tempfile.NamedTemporaryFile(mode="w+", delete=False) as dest:
+ with io.open(path) as source:
+ yield (source, dest) # type: ignore
+ except BaseException:
+ if os.path.isfile(dest.name):
+ os.unlink(dest.name)
+ raise
+ else:
+ shutil.move(dest.name, path)
+
+
+def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always", export=False):
+ # type: (_PathLike, Text, Text, Text, bool) -> Tuple[Optional[bool], Text, Text]
+ """
+ Adds or Updates a key/value to the given .env
+
+ If the .env path given doesn't exist, fails instead of risking creating
+ an orphan .env somewhere in the filesystem
+ """
+ value_to_set = value_to_set.strip("'").strip('"')
+
+ if " " in value_to_set:
+ quote_mode = "always"
+
+ if quote_mode == "always":
+ value_out = '"{}"'.format(value_to_set.replace('"', '\\"'))
+ else:
+ value_out = value_to_set
+ if export:
+ line_out = 'export {}={}\n'.format(key_to_set, value_out)
+ else:
+ line_out = "{}={}\n".format(key_to_set, value_out)
+
+ with rewrite(dotenv_path) as (source, dest):
+ replaced = False
+ for mapping in with_warn_for_invalid_lines(parse_stream(source)):
+ if mapping.key == key_to_set:
+ dest.write(line_out)
+ replaced = True
+ else:
+ dest.write(mapping.original.string)
+ if not replaced:
+ dest.write(line_out)
+
+ return True, key_to_set, value_to_set
+
+
+def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
+ # type: (_PathLike, Text, Text) -> Tuple[Optional[bool], Text]
+ """
+ Removes a given key from the given .env
+
+ If the .env path given doesn't exist, fails
+ If the given key doesn't exist in the .env, fails
+ """
+ if not os.path.exists(dotenv_path):
+ logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path)
+ return None, key_to_unset
+
+ removed = False
+ with rewrite(dotenv_path) as (source, dest):
+ for mapping in with_warn_for_invalid_lines(parse_stream(source)):
+ if mapping.key == key_to_unset:
+ removed = True
+ else:
+ dest.write(mapping.original.string)
+
+ if not removed:
+ logger.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path)
+ return None, key_to_unset
+
+ return removed, key_to_unset
+
+
+def resolve_variables(values, override):
+ # type: (Iterable[Tuple[Text, Optional[Text]]], bool) -> Mapping[Text, Optional[Text]]
+
+ new_values = {} # type: Dict[Text, Optional[Text]]
+
+ for (name, value) in values:
+ if value is None:
+ result = None
+ else:
+ atoms = parse_variables(value)
+ env = {} # type: Dict[Text, Optional[Text]]
+ if override:
+ env.update(os.environ) # type: ignore
+ env.update(new_values)
+ else:
+ env.update(new_values)
+ env.update(os.environ) # type: ignore
+ result = "".join(atom.resolve(env) for atom in atoms)
+
+ new_values[name] = result
+
+ return new_values
+
+
+def _walk_to_root(path):
+ # type: (Text) -> Iterator[Text]
+ """
+ Yield directories starting from the given directory up to the root
+ """
+ if not os.path.exists(path):
+ raise IOError('Starting path not found')
+
+ if os.path.isfile(path):
+ path = os.path.dirname(path)
+
+ last_dir = None
+ current_dir = os.path.abspath(path)
+ while last_dir != current_dir:
+ yield current_dir
+ parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
+ last_dir, current_dir = current_dir, parent_dir
+
+
+def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
+ # type: (Text, bool, bool) -> Text
+ """
+ Search in increasingly higher folders for the given file
+
+ Returns path to the file if found, or an empty string otherwise
+ """
+
+ def _is_interactive():
+ """ Decide whether this is running in a REPL or IPython notebook """
+ main = __import__('__main__', None, None, fromlist=['__file__'])
+ return not hasattr(main, '__file__')
+
+ if usecwd or _is_interactive() or getattr(sys, 'frozen', False):
+ # Should work without __file__, e.g. in REPL or IPython notebook.
+ path = os.getcwd()
+ else:
+ # will work for .py files
+ frame = sys._getframe()
+ # find first frame that is outside of this file
+ if PY2 and not __file__.endswith('.py'):
+ # in Python2 __file__ extension could be .pyc or .pyo (this doesn't account
+ # for edge case of Python compiled for non-standard extension)
+ current_file = __file__.rsplit('.', 1)[0] + '.py'
+ else:
+ current_file = __file__
+
+ while frame.f_code.co_filename == current_file:
+ assert frame.f_back is not None
+ frame = frame.f_back
+ frame_filename = frame.f_code.co_filename
+ path = os.path.dirname(os.path.abspath(frame_filename))
+
+ for dirname in _walk_to_root(path):
+ check_path = os.path.join(dirname, filename)
+ if os.path.isfile(check_path):
+ return check_path
+
+ if raise_error_if_not_found:
+ raise IOError('File not found')
+
+ return ''
+
+
+def load_dotenv(
+ dotenv_path=None,
+ stream=None,
+ verbose=False,
+ override=False,
+ interpolate=True,
+ encoding="utf-8",
+):
+ # type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, bool, Optional[Text]) -> bool # noqa
+ """Parse a .env file and then load all the variables found as environment variables.
+
+ - *dotenv_path*: absolute or relative path to .env file.
+ - *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`.
+ - *verbose*: whether to output a warning the .env file is missing. Defaults to
+ `False`.
+ - *override*: whether to override the system environment variables with the variables
+ in `.env` file. Defaults to `False`.
+ - *encoding*: encoding to be used to read the file.
+
+ If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file.
+ """
+ f = dotenv_path or stream or find_dotenv()
+ dotenv = DotEnv(
+ f,
+ verbose=verbose,
+ interpolate=interpolate,
+ override=override,
+ encoding=encoding,
+ )
+ return dotenv.set_as_environment_variables()
+
+
+def dotenv_values(
+ dotenv_path=None,
+ stream=None,
+ verbose=False,
+ interpolate=True,
+ encoding="utf-8",
+):
+ # type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, Optional[Text]) -> Dict[Text, Optional[Text]] # noqa: E501
+ """
+ Parse a .env file and return its content as a dict.
+
+ - *dotenv_path*: absolute or relative path to .env file.
+ - *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`.
+ - *verbose*: whether to output a warning the .env file is missing. Defaults to
+ `False`.
+ in `.env` file. Defaults to `False`.
+ - *encoding*: encoding to be used to read the file.
+
+ If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file.
+ """
+ f = dotenv_path or stream or find_dotenv()
+ return DotEnv(
+ f,
+ verbose=verbose,
+ interpolate=interpolate,
+ override=True,
+ encoding=encoding,
+ ).dict()
diff --git a/mixly/tools/python/dotenv/parser.py b/mixly/tools/python/dotenv/parser.py
new file mode 100644
index 00000000..5cb1cdfa
--- /dev/null
+++ b/mixly/tools/python/dotenv/parser.py
@@ -0,0 +1,231 @@
+import codecs
+import re
+
+from .compat import IS_TYPE_CHECKING, to_text
+
+if IS_TYPE_CHECKING:
+ from typing import ( # noqa:F401
+ IO, Iterator, Match, NamedTuple, Optional, Pattern, Sequence, Text,
+ Tuple
+ )
+
+
+def make_regex(string, extra_flags=0):
+ # type: (str, int) -> Pattern[Text]
+ return re.compile(to_text(string), re.UNICODE | extra_flags)
+
+
+_newline = make_regex(r"(\r\n|\n|\r)")
+_multiline_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE)
+_whitespace = make_regex(r"[^\S\r\n]*")
+_export = make_regex(r"(?:export[^\S\r\n]+)?")
+_single_quoted_key = make_regex(r"'([^']+)'")
+_unquoted_key = make_regex(r"([^=\#\s]+)")
+_equal_sign = make_regex(r"(=[^\S\r\n]*)")
+_single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'")
+_double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"')
+_unquoted_value = make_regex(r"([^\r\n]*)")
+_comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?")
+_end_of_line = make_regex(r"[^\S\r\n]*(?:\r\n|\n|\r|$)")
+_rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?")
+_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]")
+_single_quote_escapes = make_regex(r"\\[\\']")
+
+
+try:
+ # this is necessary because we only import these from typing
+ # when we are type checking, and the linter is upset if we
+ # re-import
+ import typing
+
+ Original = typing.NamedTuple(
+ "Original",
+ [
+ ("string", typing.Text),
+ ("line", int),
+ ],
+ )
+
+ Binding = typing.NamedTuple(
+ "Binding",
+ [
+ ("key", typing.Optional[typing.Text]),
+ ("value", typing.Optional[typing.Text]),
+ ("original", Original),
+ ("error", bool),
+ ],
+ )
+except (ImportError, AttributeError):
+ from collections import namedtuple
+ Original = namedtuple( # type: ignore
+ "Original",
+ [
+ "string",
+ "line",
+ ],
+ )
+ Binding = namedtuple( # type: ignore
+ "Binding",
+ [
+ "key",
+ "value",
+ "original",
+ "error",
+ ],
+ )
+
+
+class Position:
+ def __init__(self, chars, line):
+ # type: (int, int) -> None
+ self.chars = chars
+ self.line = line
+
+ @classmethod
+ def start(cls):
+ # type: () -> Position
+ return cls(chars=0, line=1)
+
+ def set(self, other):
+ # type: (Position) -> None
+ self.chars = other.chars
+ self.line = other.line
+
+ def advance(self, string):
+ # type: (Text) -> None
+ self.chars += len(string)
+ self.line += len(re.findall(_newline, string))
+
+
+class Error(Exception):
+ pass
+
+
+class Reader:
+ def __init__(self, stream):
+ # type: (IO[Text]) -> None
+ self.string = stream.read()
+ self.position = Position.start()
+ self.mark = Position.start()
+
+ def has_next(self):
+ # type: () -> bool
+ return self.position.chars < len(self.string)
+
+ def set_mark(self):
+ # type: () -> None
+ self.mark.set(self.position)
+
+ def get_marked(self):
+ # type: () -> Original
+ return Original(
+ string=self.string[self.mark.chars:self.position.chars],
+ line=self.mark.line,
+ )
+
+ def peek(self, count):
+ # type: (int) -> Text
+ return self.string[self.position.chars:self.position.chars + count]
+
+ def read(self, count):
+ # type: (int) -> Text
+ result = self.string[self.position.chars:self.position.chars + count]
+ if len(result) < count:
+ raise Error("read: End of string")
+ self.position.advance(result)
+ return result
+
+ def read_regex(self, regex):
+ # type: (Pattern[Text]) -> Sequence[Text]
+ match = regex.match(self.string, self.position.chars)
+ if match is None:
+ raise Error("read_regex: Pattern not found")
+ self.position.advance(self.string[match.start():match.end()])
+ return match.groups()
+
+
+def decode_escapes(regex, string):
+ # type: (Pattern[Text], Text) -> Text
+ def decode_match(match):
+ # type: (Match[Text]) -> Text
+ return codecs.decode(match.group(0), 'unicode-escape') # type: ignore
+
+ return regex.sub(decode_match, string)
+
+
+def parse_key(reader):
+ # type: (Reader) -> Optional[Text]
+ char = reader.peek(1)
+ if char == "#":
+ return None
+ elif char == "'":
+ (key,) = reader.read_regex(_single_quoted_key)
+ else:
+ (key,) = reader.read_regex(_unquoted_key)
+ return key
+
+
+def parse_unquoted_value(reader):
+ # type: (Reader) -> Text
+ (part,) = reader.read_regex(_unquoted_value)
+ return re.sub(r"\s+#.*", "", part).rstrip()
+
+
+def parse_value(reader):
+ # type: (Reader) -> Text
+ char = reader.peek(1)
+ if char == u"'":
+ (value,) = reader.read_regex(_single_quoted_value)
+ return decode_escapes(_single_quote_escapes, value)
+ elif char == u'"':
+ (value,) = reader.read_regex(_double_quoted_value)
+ return decode_escapes(_double_quote_escapes, value)
+ elif char in (u"", u"\n", u"\r"):
+ return u""
+ else:
+ return parse_unquoted_value(reader)
+
+
+def parse_binding(reader):
+ # type: (Reader) -> Binding
+ reader.set_mark()
+ try:
+ reader.read_regex(_multiline_whitespace)
+ if not reader.has_next():
+ return Binding(
+ key=None,
+ value=None,
+ original=reader.get_marked(),
+ error=False,
+ )
+ reader.read_regex(_export)
+ key = parse_key(reader)
+ reader.read_regex(_whitespace)
+ if reader.peek(1) == "=":
+ reader.read_regex(_equal_sign)
+ value = parse_value(reader) # type: Optional[Text]
+ else:
+ value = None
+ reader.read_regex(_comment)
+ reader.read_regex(_end_of_line)
+ return Binding(
+ key=key,
+ value=value,
+ original=reader.get_marked(),
+ error=False,
+ )
+ except Error:
+ reader.read_regex(_rest_of_line)
+ return Binding(
+ key=None,
+ value=None,
+ original=reader.get_marked(),
+ error=True,
+ )
+
+
+def parse_stream(stream):
+ # type: (IO[Text]) -> Iterator[Binding]
+ reader = Reader(stream)
+ while reader.has_next():
+ yield parse_binding(reader)
diff --git a/mixly/tools/python/dotenv/py.typed b/mixly/tools/python/dotenv/py.typed
new file mode 100644
index 00000000..7632ecf7
--- /dev/null
+++ b/mixly/tools/python/dotenv/py.typed
@@ -0,0 +1 @@
+# Marker file for PEP 561
diff --git a/mixly/tools/python/dotenv/variables.py b/mixly/tools/python/dotenv/variables.py
new file mode 100644
index 00000000..4828dfc2
--- /dev/null
+++ b/mixly/tools/python/dotenv/variables.py
@@ -0,0 +1,106 @@
+import re
+from abc import ABCMeta
+
+from .compat import IS_TYPE_CHECKING
+
+if IS_TYPE_CHECKING:
+ from typing import Iterator, Mapping, Optional, Pattern, Text
+
+
+_posix_variable = re.compile(
+ r"""
+ \$\{
+ (?P[^\}:]*)
+ (?::-
+ (?P[^\}]*)
+ )?
+ \}
+ """,
+ re.VERBOSE,
+) # type: Pattern[Text]
+
+
+class Atom():
+ __metaclass__ = ABCMeta
+
+ def __ne__(self, other):
+ # type: (object) -> bool
+ result = self.__eq__(other)
+ if result is NotImplemented:
+ return NotImplemented
+ return not result
+
+ def resolve(self, env):
+ # type: (Mapping[Text, Optional[Text]]) -> Text
+ raise NotImplementedError
+
+
+class Literal(Atom):
+ def __init__(self, value):
+ # type: (Text) -> None
+ self.value = value
+
+ def __repr__(self):
+ # type: () -> str
+ return "Literal(value={})".format(self.value)
+
+ def __eq__(self, other):
+ # type: (object) -> bool
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return self.value == other.value
+
+ def __hash__(self):
+ # type: () -> int
+ return hash((self.__class__, self.value))
+
+ def resolve(self, env):
+ # type: (Mapping[Text, Optional[Text]]) -> Text
+ return self.value
+
+
+class Variable(Atom):
+ def __init__(self, name, default):
+ # type: (Text, Optional[Text]) -> None
+ self.name = name
+ self.default = default
+
+ def __repr__(self):
+ # type: () -> str
+ return "Variable(name={}, default={})".format(self.name, self.default)
+
+ def __eq__(self, other):
+ # type: (object) -> bool
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return (self.name, self.default) == (other.name, other.default)
+
+ def __hash__(self):
+ # type: () -> int
+ return hash((self.__class__, self.name, self.default))
+
+ def resolve(self, env):
+ # type: (Mapping[Text, Optional[Text]]) -> Text
+ default = self.default if self.default is not None else ""
+ result = env.get(self.name, default)
+ return result if result is not None else ""
+
+
+def parse_variables(value):
+ # type: (Text) -> Iterator[Atom]
+ cursor = 0
+
+ for match in _posix_variable.finditer(value):
+ (start, end) = match.span()
+ name = match.groupdict()["name"]
+ default = match.groupdict()["default"]
+
+ if start > cursor:
+ yield Literal(value=value[cursor:start])
+
+ yield Variable(name=name, default=default)
+ cursor = end
+
+ length = len(value)
+ if cursor < length:
+ yield Literal(value=value[cursor:length])
diff --git a/mixly/tools/python/dotenv/version.py b/mixly/tools/python/dotenv/version.py
new file mode 100644
index 00000000..c6eae9f8
--- /dev/null
+++ b/mixly/tools/python/dotenv/version.py
@@ -0,0 +1 @@
+__version__ = "0.17.1"
diff --git a/mixly/tools/python/esptool/__init__.py b/mixly/tools/python/esptool/__init__.py
new file mode 100644
index 00000000..7f919bc3
--- /dev/null
+++ b/mixly/tools/python/esptool/__init__.py
@@ -0,0 +1,1337 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+# PYTHON_ARGCOMPLETE_OK
+__all__ = [
+ "chip_id",
+ "detect_chip",
+ "dump_mem",
+ "elf2image",
+ "erase_flash",
+ "erase_region",
+ "flash_id",
+ "get_security_info",
+ "image_info",
+ "load_ram",
+ "make_image",
+ "merge_bin",
+ "read_flash",
+ "read_flash_status",
+ "read_mac",
+ "read_mem",
+ "run",
+ "verify_flash",
+ "version",
+ "write_flash",
+ "write_flash_status",
+ "write_mem",
+]
+
+__version__ = "4.8.1"
+
+import argparse
+import inspect
+import os
+import shlex
+import sys
+import time
+import traceback
+
+from esptool.bin_image import intel_hex_to_bin
+from esptool.cmds import (
+ DETECTED_FLASH_SIZES,
+ chip_id,
+ detect_chip,
+ detect_flash_size,
+ dump_mem,
+ elf2image,
+ erase_flash,
+ erase_region,
+ flash_id,
+ read_flash_sfdp,
+ get_security_info,
+ image_info,
+ load_ram,
+ make_image,
+ merge_bin,
+ read_flash,
+ read_flash_status,
+ read_mac,
+ read_mem,
+ run,
+ verify_flash,
+ version,
+ write_flash,
+ write_flash_status,
+ write_mem,
+)
+from esptool.config import load_config_file
+from esptool.loader import (
+ DEFAULT_CONNECT_ATTEMPTS,
+ DEFAULT_OPEN_PORT_ATTEMPTS,
+ StubFlasher,
+ ESPLoader,
+ list_ports,
+)
+from esptool.targets import CHIP_DEFS, CHIP_LIST, ESP32ROM
+from esptool.util import (
+ FatalError,
+ NotImplementedInROMError,
+ flash_size_bytes,
+ strip_chip_name,
+)
+from itertools import chain, cycle, repeat
+
+import serial
+
+
+def main(argv=None, esp=None):
+ """
+ Main function for esptool
+
+ argv - Optional override for default arguments parsing (that uses sys.argv),
+ can be a list of custom arguments as strings. Arguments and their values
+ need to be added as individual items to the list
+ e.g. "-b 115200" thus becomes ['-b', '115200'].
+
+ esp - Optional override of the connected device previously
+ returned by get_default_connected_device()
+ """
+
+ external_esp = esp is not None
+
+ parser = argparse.ArgumentParser(
+ description="esptool.py v%s - Espressif chips ROM Bootloader Utility"
+ % __version__,
+ prog="esptool",
+ )
+
+ parser.add_argument(
+ "--chip",
+ "-c",
+ help="Target chip type",
+ type=strip_chip_name,
+ choices=["auto"] + CHIP_LIST,
+ default=os.environ.get("ESPTOOL_CHIP", "auto"),
+ )
+
+ parser.add_argument(
+ "--port",
+ "-p",
+ help="Serial port device",
+ default=os.environ.get("ESPTOOL_PORT", None),
+ )
+
+ parser.add_argument(
+ "--baud",
+ "-b",
+ help="Serial port baud rate used when flashing/reading",
+ type=arg_auto_int,
+ default=os.environ.get("ESPTOOL_BAUD", ESPLoader.ESP_ROM_BAUD),
+ )
+
+ parser.add_argument(
+ "--port-filter",
+ action="append",
+ help="Serial port device filter, can be vid=NUMBER, pid=NUMBER, name=SUBSTRING",
+ type=str,
+ default=[],
+ )
+
+ parser.add_argument(
+ "--before",
+ help="What to do before connecting to the chip",
+ choices=["default_reset", "usb_reset", "no_reset", "no_reset_no_sync"],
+ default=os.environ.get("ESPTOOL_BEFORE", "default_reset"),
+ )
+
+ parser.add_argument(
+ "--after",
+ "-a",
+ help="What to do after esptool.py is finished",
+ choices=["hard_reset", "soft_reset", "no_reset", "no_reset_stub"],
+ default=os.environ.get("ESPTOOL_AFTER", "hard_reset"),
+ )
+
+ parser.add_argument(
+ "--no-stub",
+ help="Disable launching the flasher stub, only talk to ROM bootloader. "
+ "Some features will not be available.",
+ action="store_true",
+ )
+
+ # --stub-version can be set with --no-stub so the tests wouldn't fail if this option is implied globally
+ parser.add_argument(
+ "--stub-version",
+ default=os.environ.get("ESPTOOL_STUB_VERSION", StubFlasher.STUB_SUBDIRS[0]),
+ choices=StubFlasher.STUB_SUBDIRS,
+ # not a public option and is not subject to the semantic versioning policy
+ help=argparse.SUPPRESS,
+ )
+
+ parser.add_argument(
+ "--trace",
+ "-t",
+ help="Enable trace-level output of esptool.py interactions.",
+ action="store_true",
+ )
+
+ parser.add_argument(
+ "--override-vddsdio",
+ help="Override ESP32 VDDSDIO internal voltage regulator (use with care)",
+ choices=ESP32ROM.OVERRIDE_VDDSDIO_CHOICES,
+ nargs="?",
+ )
+
+ parser.add_argument(
+ "--connect-attempts",
+ help=(
+ "Number of attempts to connect, negative or 0 for infinite. "
+ "Default: %d." % DEFAULT_CONNECT_ATTEMPTS
+ ),
+ type=int,
+ default=os.environ.get("ESPTOOL_CONNECT_ATTEMPTS", DEFAULT_CONNECT_ATTEMPTS),
+ )
+
+ subparsers = parser.add_subparsers(
+ dest="operation", help="Run esptool.py {command} -h for additional help"
+ )
+
+ def add_spi_connection_arg(parent):
+ parent.add_argument(
+ "--spi-connection",
+ "-sc",
+ help="Override default SPI Flash connection. "
+ "Value can be SPI, HSPI or a comma-separated list of 5 I/O numbers "
+ "to use for SPI flash (CLK,Q,D,HD,CS). Not supported with ESP8266.",
+ action=SpiConnectionAction,
+ )
+
+ parser_load_ram = subparsers.add_parser(
+ "load_ram", help="Download an image to RAM and execute"
+ )
+ parser_load_ram.add_argument(
+ "filename", help="Firmware image", action=AutoHex2BinAction
+ )
+
+ parser_dump_mem = subparsers.add_parser(
+ "dump_mem", help="Dump arbitrary memory to disk"
+ )
+ parser_dump_mem.add_argument("address", help="Base address", type=arg_auto_int)
+ parser_dump_mem.add_argument(
+ "size", help="Size of region to dump", type=arg_auto_int
+ )
+ parser_dump_mem.add_argument("filename", help="Name of binary dump")
+
+ parser_read_mem = subparsers.add_parser(
+ "read_mem", help="Read arbitrary memory location"
+ )
+ parser_read_mem.add_argument("address", help="Address to read", type=arg_auto_int)
+
+ parser_write_mem = subparsers.add_parser(
+ "write_mem", help="Read-modify-write to arbitrary memory location"
+ )
+ parser_write_mem.add_argument("address", help="Address to write", type=arg_auto_int)
+ parser_write_mem.add_argument("value", help="Value", type=arg_auto_int)
+ parser_write_mem.add_argument(
+ "mask",
+ help="Mask of bits to write",
+ type=arg_auto_int,
+ nargs="?",
+ default="0xFFFFFFFF",
+ )
+
+ def add_spi_flash_subparsers(
+ parent: argparse.ArgumentParser,
+ allow_keep: bool,
+ auto_detect: bool,
+ size_only: bool = False,
+ ):
+ """Add common parser arguments for SPI flash properties"""
+ extra_keep_args = ["keep"] if allow_keep else []
+
+ if auto_detect and allow_keep:
+ extra_fs_message = ", detect, or keep"
+ flash_sizes = ["detect", "keep"]
+ elif auto_detect:
+ extra_fs_message = ", or detect"
+ flash_sizes = ["detect"]
+ elif allow_keep:
+ extra_fs_message = ", or keep"
+ flash_sizes = ["keep"]
+ else:
+ extra_fs_message = ""
+ flash_sizes = []
+
+ if not size_only:
+ parent.add_argument(
+ "--flash_freq",
+ "-ff",
+ help="SPI Flash frequency",
+ choices=extra_keep_args
+ + [
+ "80m",
+ "60m",
+ "48m",
+ "40m",
+ "30m",
+ "26m",
+ "24m",
+ "20m",
+ "16m",
+ "15m",
+ "12m",
+ ],
+ default=os.environ.get("ESPTOOL_FF", "keep" if allow_keep else None),
+ )
+ parent.add_argument(
+ "--flash_mode",
+ "-fm",
+ help="SPI Flash mode",
+ choices=extra_keep_args + ["qio", "qout", "dio", "dout"],
+ default=os.environ.get("ESPTOOL_FM", "keep" if allow_keep else "qio"),
+ )
+
+ parent.add_argument(
+ "--flash_size",
+ "-fs",
+ help="SPI Flash size in MegaBytes "
+ "(1MB, 2MB, 4MB, 8MB, 16MB, 32MB, 64MB, 128MB) "
+ "plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)" + extra_fs_message,
+ choices=flash_sizes
+ + [
+ "256KB",
+ "512KB",
+ "1MB",
+ "2MB",
+ "2MB-c1",
+ "4MB",
+ "4MB-c1",
+ "8MB",
+ "16MB",
+ "32MB",
+ "64MB",
+ "128MB",
+ ],
+ default=os.environ.get("ESPTOOL_FS", "keep" if allow_keep else "1MB"),
+ )
+ add_spi_connection_arg(parent)
+
+ parser_write_flash = subparsers.add_parser(
+ "write_flash", help="Write a binary blob to flash"
+ )
+
+ parser_write_flash.add_argument(
+ "addr_filename",
+ metavar=" ",
+ help="Address followed by binary filename, separated by space",
+ action=AddrFilenamePairAction,
+ )
+ parser_write_flash.add_argument(
+ "--erase-all",
+ "-e",
+ help="Erase all regions of flash (not just write areas) before programming",
+ action="store_true",
+ )
+
+ add_spi_flash_subparsers(parser_write_flash, allow_keep=True, auto_detect=True)
+ parser_write_flash.add_argument(
+ "--no-progress", "-p", help="Suppress progress output", action="store_true"
+ )
+ parser_write_flash.add_argument(
+ "--verify",
+ help="Verify just-written data on flash "
+ "(mostly superfluous, data is read back during flashing)",
+ action="store_true",
+ )
+ parser_write_flash.add_argument(
+ "--encrypt",
+ help="Apply flash encryption when writing data "
+ "(required correct efuse settings)",
+ action="store_true",
+ )
+ # In order to not break backward compatibility,
+ # our list of encrypted files to flash is a new parameter
+ parser_write_flash.add_argument(
+ "--encrypt-files",
+ metavar=" ",
+ help="Files to be encrypted on the flash. "
+ "Address followed by binary filename, separated by space.",
+ action=AddrFilenamePairAction,
+ )
+ parser_write_flash.add_argument(
+ "--ignore-flash-encryption-efuse-setting",
+ help="Ignore flash encryption efuse settings ",
+ action="store_true",
+ )
+ parser_write_flash.add_argument(
+ "--force",
+ help="Force write, skip security and compatibility checks. Use with caution!",
+ action="store_true",
+ )
+
+ compress_args = parser_write_flash.add_mutually_exclusive_group(required=False)
+ compress_args.add_argument(
+ "--compress",
+ "-z",
+ help="Compress data in transfer (default unless --no-stub is specified)",
+ action="store_true",
+ default=None,
+ )
+ compress_args.add_argument(
+ "--no-compress",
+ "-u",
+ help="Disable data compression during transfer "
+ "(default if --no-stub is specified)",
+ action="store_true",
+ )
+
+ subparsers.add_parser("run", help="Run application code in flash")
+
+ parser_image_info = subparsers.add_parser(
+ "image_info", help="Dump headers from a binary file (bootloader or application)"
+ )
+ parser_image_info.add_argument(
+ "filename", help="Image file to parse", action=AutoHex2BinAction
+ )
+ parser_image_info.add_argument(
+ "--version",
+ "-v",
+ help="Output format version (1 - legacy, 2 - extended)",
+ choices=["1", "2"],
+ default="1",
+ )
+
+ parser_make_image = subparsers.add_parser(
+ "make_image", help="Create an application image from binary files"
+ )
+ parser_make_image.add_argument("output", help="Output image file")
+ parser_make_image.add_argument(
+ "--segfile", "-f", action="append", help="Segment input file"
+ )
+ parser_make_image.add_argument(
+ "--segaddr",
+ "-a",
+ action="append",
+ help="Segment base address",
+ type=arg_auto_int,
+ )
+ parser_make_image.add_argument(
+ "--entrypoint",
+ "-e",
+ help="Address of entry point",
+ type=arg_auto_int,
+ default=0,
+ )
+
+ parser_elf2image = subparsers.add_parser(
+ "elf2image", help="Create an application image from ELF file"
+ )
+ parser_elf2image.add_argument("input", help="Input ELF file")
+ parser_elf2image.add_argument(
+ "--output",
+ "-o",
+ help="Output filename prefix (for version 1 image), "
+ "or filename (for version 2 single image)",
+ type=str,
+ )
+ parser_elf2image.add_argument(
+ "--version",
+ "-e",
+ help="Output image version",
+ choices=["1", "2", "3"],
+ default="1",
+ )
+ parser_elf2image.add_argument(
+ # it kept for compatibility
+ # Minimum chip revision (deprecated, consider using --min-rev-full)
+ "--min-rev",
+ "-r",
+ help=argparse.SUPPRESS,
+ type=int,
+ choices=range(256),
+ metavar="{0, ... 255}",
+ default=0,
+ )
+ parser_elf2image.add_argument(
+ "--min-rev-full",
+ help="Minimal chip revision (in format: major * 100 + minor)",
+ type=int,
+ choices=range(65536),
+ metavar="{0, ... 65535}",
+ default=0,
+ )
+ parser_elf2image.add_argument(
+ "--max-rev-full",
+ help="Maximal chip revision (in format: major * 100 + minor)",
+ type=int,
+ choices=range(65536),
+ metavar="{0, ... 65535}",
+ default=65535,
+ )
+ parser_elf2image.add_argument(
+ "--secure-pad",
+ action="store_true",
+ help="Pad image so once signed it will end on a 64KB boundary. "
+ "For Secure Boot v1 images only.",
+ )
+ parser_elf2image.add_argument(
+ "--secure-pad-v2",
+ action="store_true",
+ help="Pad image to 64KB, so once signed its signature sector will"
+ "start at the next 64K block. For Secure Boot v2 images only.",
+ )
+ parser_elf2image.add_argument(
+ "--elf-sha256-offset",
+ help="If set, insert SHA256 hash (32 bytes) of the input ELF file "
+ "at specified offset in the binary.",
+ type=arg_auto_int,
+ default=None,
+ )
+ parser_elf2image.add_argument(
+ "--dont-append-digest",
+ dest="append_digest",
+ help="Don't append a SHA256 digest of the entire image after the checksum. "
+ "This argument is not supported and ignored for ESP8266.",
+ action="store_false",
+ default=True,
+ )
+ parser_elf2image.add_argument(
+ "--use_segments",
+ help="If set, ELF segments will be used instead of ELF sections "
+ "to generate the image.",
+ action="store_true",
+ )
+ parser_elf2image.add_argument(
+ "--flash-mmu-page-size",
+ help="Change flash MMU page size.",
+ choices=["64KB", "32KB", "16KB", "8KB"],
+ )
+ parser_elf2image.add_argument(
+ "--pad-to-size",
+ help="The block size with which the final binary image after padding "
+ "must be aligned to. Value 0xFF is used for padding, similar to erase_flash",
+ default=None,
+ )
+ parser_elf2image.add_argument(
+ "--ram-only-header",
+ help="Order segments of the output so IRAM and DRAM are placed at the "
+ "beginning and force the main header segment number to RAM segments "
+ "quantity. This will make the other segments invisible to the ROM "
+ "loader. Use this argument with care because the ROM loader will load "
+ "only the RAM segments although the other segments being present in "
+ "the output. Implies --dont-append-digest",
+ action="store_true",
+ default=None,
+ )
+
+ add_spi_flash_subparsers(parser_elf2image, allow_keep=False, auto_detect=False)
+
+ subparsers.add_parser("read_mac", help="Read MAC address from OTP ROM")
+
+ subparsers.add_parser("chip_id", help="Read Chip ID from OTP ROM")
+
+ parser_flash_id = subparsers.add_parser(
+ "flash_id", help="Read SPI flash manufacturer and device ID"
+ )
+ add_spi_connection_arg(parser_flash_id)
+
+ parser_read_status = subparsers.add_parser(
+ "read_flash_status", help="Read SPI flash status register"
+ )
+
+ add_spi_connection_arg(parser_read_status)
+ parser_read_status.add_argument(
+ "--bytes",
+ help="Number of bytes to read (1-3)",
+ type=int,
+ choices=[1, 2, 3],
+ default=2,
+ )
+
+ parser_write_status = subparsers.add_parser(
+ "write_flash_status", help="Write SPI flash status register"
+ )
+
+ add_spi_connection_arg(parser_write_status)
+ parser_write_status.add_argument(
+ "--non-volatile",
+ help="Write non-volatile bits (use with caution)",
+ action="store_true",
+ )
+ parser_write_status.add_argument(
+ "--bytes",
+ help="Number of status bytes to write (1-3)",
+ type=int,
+ choices=[1, 2, 3],
+ default=2,
+ )
+ parser_write_status.add_argument("value", help="New value", type=arg_auto_int)
+
+ parser_read_flash = subparsers.add_parser(
+ "read_flash", help="Read SPI flash content"
+ )
+ add_spi_flash_subparsers(
+ parser_read_flash, allow_keep=True, auto_detect=True, size_only=True
+ )
+ parser_read_flash.add_argument("address", help="Start address", type=arg_auto_int)
+ parser_read_flash.add_argument(
+ "size",
+ help="Size of region to dump. Use `ALL` to read to the end of flash.",
+ type=arg_auto_size,
+ )
+ parser_read_flash.add_argument("filename", help="Name of binary dump")
+ parser_read_flash.add_argument(
+ "--no-progress", "-p", help="Suppress progress output", action="store_true"
+ )
+
+ parser_verify_flash = subparsers.add_parser(
+ "verify_flash", help="Verify a binary blob against flash"
+ )
+ parser_verify_flash.add_argument(
+ "addr_filename",
+ help="Address and binary file to verify there, separated by space",
+ action=AddrFilenamePairAction,
+ )
+ parser_verify_flash.add_argument(
+ "--diff", "-d", help="Show differences", choices=["no", "yes"], default="no"
+ )
+ add_spi_flash_subparsers(parser_verify_flash, allow_keep=True, auto_detect=True)
+
+ parser_erase_flash = subparsers.add_parser(
+ "erase_flash", help="Perform Chip Erase on SPI flash"
+ )
+ parser_erase_flash.add_argument(
+ "--force",
+ help="Erase flash even if security features are enabled. Use with caution!",
+ action="store_true",
+ )
+ add_spi_connection_arg(parser_erase_flash)
+
+ parser_erase_region = subparsers.add_parser(
+ "erase_region", help="Erase a region of the flash"
+ )
+ parser_erase_region.add_argument(
+ "--force",
+ help="Erase region even if security features are enabled. Use with caution!",
+ action="store_true",
+ )
+ add_spi_connection_arg(parser_erase_region)
+ parser_erase_region.add_argument(
+ "address", help="Start address (must be multiple of 4096)", type=arg_auto_int
+ )
+ parser_erase_region.add_argument(
+ "size",
+ help="Size of region to erase (must be multiple of 4096). "
+ "Use `ALL` to erase to the end of flash.",
+ type=arg_auto_size,
+ )
+
+ parser_read_flash_sfdp = subparsers.add_parser(
+ "read_flash_sfdp",
+ help="Read SPI flash SFDP (Serial Flash Discoverable Parameters)",
+ )
+ add_spi_flash_subparsers(parser_read_flash_sfdp, allow_keep=True, auto_detect=True)
+ parser_read_flash_sfdp.add_argument("addr", type=arg_auto_int)
+ parser_read_flash_sfdp.add_argument("bytes", type=int)
+
+ parser_merge_bin = subparsers.add_parser(
+ "merge_bin",
+ help="Merge multiple raw binary files into a single file for later flashing",
+ )
+
+ parser_merge_bin.add_argument(
+ "--output", "-o", help="Output filename", type=str, required=True
+ )
+ parser_merge_bin.add_argument(
+ "--format",
+ "-f",
+ help="Format of the output file",
+ choices=["raw", "uf2", "hex"],
+ default="raw",
+ )
+ uf2_group = parser_merge_bin.add_argument_group("UF2 format")
+ uf2_group.add_argument(
+ "--chunk-size",
+ help="Specify the used data part of the 512 byte UF2 block. "
+ "A common value is 256. By default the largest possible value will be used.",
+ default=None,
+ type=arg_auto_chunk_size,
+ )
+ uf2_group.add_argument(
+ "--md5-disable",
+ help="Disable MD5 checksum in UF2 output",
+ action="store_true",
+ )
+ add_spi_flash_subparsers(parser_merge_bin, allow_keep=True, auto_detect=False)
+
+ raw_group = parser_merge_bin.add_argument_group("RAW format")
+ raw_group.add_argument(
+ "--target-offset",
+ "-t",
+ help="Target offset where the output file will be flashed",
+ type=arg_auto_int,
+ default=0,
+ )
+ raw_group.add_argument(
+ "--fill-flash-size",
+ help="If set, the final binary file will be padded with FF "
+ "bytes up to this flash size.",
+ choices=[
+ "256KB",
+ "512KB",
+ "1MB",
+ "2MB",
+ "4MB",
+ "8MB",
+ "16MB",
+ "32MB",
+ "64MB",
+ "128MB",
+ ],
+ )
+ parser_merge_bin.add_argument(
+ "addr_filename",
+ metavar=" ",
+ help="Address followed by binary filename, separated by space",
+ action=AddrFilenamePairAction,
+ )
+
+ subparsers.add_parser("get_security_info", help="Get some security-related data")
+
+ subparsers.add_parser("version", help="Print esptool version")
+
+ # internal sanity check - every operation matches a module function of the same name
+ for operation in subparsers.choices.keys():
+ assert operation in globals(), "%s should be a module function" % operation
+
+ # Enable argcomplete only on Unix-like systems
+ if sys.platform != "win32":
+ try:
+ import argcomplete
+
+ argcomplete.autocomplete(parser)
+ except ImportError:
+ pass
+
+ argv = expand_file_arguments(argv or sys.argv[1:])
+
+ args = parser.parse_args(argv)
+ print("esptool.py v%s" % __version__)
+ load_config_file(verbose=True)
+
+ StubFlasher.set_preferred_stub_subdir(args.stub_version)
+
+ # Parse filter arguments into separate lists
+ args.filterVids = []
+ args.filterPids = []
+ args.filterNames = []
+ for f in args.port_filter:
+ kvp = f.split("=")
+ if len(kvp) != 2:
+ raise FatalError("Option --port-filter argument must consist of key=value")
+ if kvp[0] == "vid":
+ args.filterVids.append(arg_auto_int(kvp[1]))
+ elif kvp[0] == "pid":
+ args.filterPids.append(arg_auto_int(kvp[1]))
+ elif kvp[0] == "name":
+ args.filterNames.append(kvp[1])
+ else:
+ raise FatalError("Option --port-filter argument key not recognized")
+
+ # operation function can take 1 arg (args), 2 args (esp, arg)
+ # or be a member function of the ESPLoader class.
+
+ if args.operation is None:
+ parser.print_help()
+ sys.exit(1)
+
+ # Forbid the usage of both --encrypt, which means encrypt all the given files,
+ # and --encrypt-files, which represents the list of files to encrypt.
+ # The reason is that allowing both at the same time increases the chances of
+ # having contradictory lists (e.g. one file not available in one of list).
+ if (
+ args.operation == "write_flash"
+ and args.encrypt
+ and args.encrypt_files is not None
+ ):
+ raise FatalError(
+ "Options --encrypt and --encrypt-files "
+ "must not be specified at the same time."
+ )
+
+ operation_func = globals()[args.operation]
+ operation_args = inspect.getfullargspec(operation_func).args
+
+ if (
+ operation_args[0] == "esp"
+ ): # operation function takes an ESPLoader connection object
+ if args.before != "no_reset_no_sync":
+ initial_baud = min(
+ ESPLoader.ESP_ROM_BAUD, args.baud
+ ) # don't sync faster than the default baud rate
+ else:
+ initial_baud = args.baud
+
+ if args.port is None:
+ ser_list = get_port_list(args.filterVids, args.filterPids, args.filterNames)
+ print("Found %d serial ports" % len(ser_list))
+ else:
+ ser_list = [args.port]
+ open_port_attempts = os.environ.get(
+ "ESPTOOL_OPEN_PORT_ATTEMPTS", DEFAULT_OPEN_PORT_ATTEMPTS
+ )
+ try:
+ open_port_attempts = int(open_port_attempts)
+ except ValueError:
+ raise SystemExit("Invalid value for ESPTOOL_OPEN_PORT_ATTEMPTS")
+ if open_port_attempts != 1:
+ if args.port is None or args.chip == "auto":
+ print(
+ "WARNING: The ESPTOOL_OPEN_PORT_ATTEMPTS (open_port_attempts) option can only be used with --port and --chip arguments."
+ )
+ else:
+ esp = esp or connect_loop(
+ args.port,
+ initial_baud,
+ args.chip,
+ open_port_attempts,
+ args.trace,
+ args.before,
+ )
+ esp = esp or get_default_connected_device(
+ ser_list,
+ port=args.port,
+ connect_attempts=args.connect_attempts,
+ initial_baud=initial_baud,
+ chip=args.chip,
+ trace=args.trace,
+ before=args.before,
+ )
+
+ if esp is None:
+ raise FatalError(
+ "Could not connect to an Espressif device "
+ "on any of the %d available serial ports." % len(ser_list)
+ )
+
+ if esp.secure_download_mode:
+ print("Chip is %s in Secure Download Mode" % esp.CHIP_NAME)
+ else:
+ print("Chip is %s" % (esp.get_chip_description()))
+ print("Features: %s" % ", ".join(esp.get_chip_features()))
+ print("Crystal is %dMHz" % esp.get_crystal_freq())
+ read_mac(esp, args)
+
+ if not args.no_stub:
+ if esp.secure_download_mode:
+ print(
+ "WARNING: Stub loader is not supported in Secure Download Mode, "
+ "setting --no-stub"
+ )
+ args.no_stub = True
+ elif not esp.IS_STUB and esp.stub_is_disabled:
+ print(
+ "WARNING: Stub loader has been disabled for compatibility, "
+ "setting --no-stub"
+ )
+ args.no_stub = True
+ else:
+ try:
+ esp = esp.run_stub()
+ except Exception:
+ # The CH9102 bridge (PID: 0x55D4) can have issues on MacOS
+ if sys.platform == "darwin" and esp._get_pid() == 0x55D4:
+ print(
+ "\nNote: If issues persist, "
+ "try installing the WCH USB-to-Serial MacOS driver."
+ )
+ raise
+
+ if args.override_vddsdio:
+ esp.override_vddsdio(args.override_vddsdio)
+
+ if args.baud > initial_baud:
+ try:
+ esp.change_baud(args.baud)
+ except NotImplementedInROMError:
+ print(
+ "WARNING: ROM doesn't support changing baud rate. "
+ "Keeping initial baud rate %d" % initial_baud
+ )
+
+ def _define_spi_conn(spi_connection):
+ """Prepare SPI configuration string and value for flash_spi_attach()"""
+ clk, q, d, hd, cs = spi_connection
+ spi_config_txt = f"CLK:{clk}, Q:{q}, D:{d}, HD:{hd}, CS:{cs}"
+ value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk
+ return spi_config_txt, value
+
+ # Override the common SPI flash parameter stuff if configured to do so
+ if hasattr(args, "spi_connection") and args.spi_connection is not None:
+ spi_config = args.spi_connection
+ if args.spi_connection == "SPI":
+ value = 0
+ elif args.spi_connection == "HSPI":
+ value = 1
+ else:
+ esp.check_spi_connection(args.spi_connection)
+ # Encode the pin numbers as a 32-bit integer with packed 6-bit values,
+ # the same way the ESP ROM takes them
+ spi_config, value = _define_spi_conn(args.spi_connection)
+ print(f"Configuring SPI flash mode ({spi_config})...")
+ esp.flash_spi_attach(value)
+ elif args.no_stub:
+ if esp.CHIP_NAME != "ESP32" or esp.secure_download_mode:
+ print("Enabling default SPI flash mode...")
+ # ROM loader doesn't enable flash unless we explicitly do it
+ esp.flash_spi_attach(0)
+ else:
+ # ROM doesn't attach in-package flash chips
+ spi_chip_pads = esp.get_chip_spi_pads()
+ spi_config_txt, value = _define_spi_conn(spi_chip_pads)
+ if spi_chip_pads != (0, 0, 0, 0, 0):
+ print(
+ "Attaching flash from eFuses' SPI pads configuration"
+ f"({spi_config_txt})..."
+ )
+ else:
+ print("Enabling default SPI flash mode...")
+ esp.flash_spi_attach(value)
+
+ # XMC chip startup sequence
+ XMC_VENDOR_ID = 0x20
+
+ def is_xmc_chip_strict():
+ id = esp.flash_id()
+ rdid = ((id & 0xFF) << 16) | ((id >> 16) & 0xFF) | (id & 0xFF00)
+
+ vendor_id = (rdid >> 16) & 0xFF
+ mfid = (rdid >> 8) & 0xFF
+ cpid = rdid & 0xFF
+
+ if vendor_id != XMC_VENDOR_ID:
+ return False
+
+ matched = False
+ if mfid == 0x40:
+ if cpid >= 0x13 and cpid <= 0x20:
+ matched = True
+ elif mfid == 0x41:
+ if cpid >= 0x17 and cpid <= 0x20:
+ matched = True
+ elif mfid == 0x50:
+ if cpid >= 0x15 and cpid <= 0x16:
+ matched = True
+ return matched
+
+ def flash_xmc_startup():
+ # If the RDID value is a valid XMC one, may skip the flow
+ fast_check = True
+ if fast_check and is_xmc_chip_strict():
+ return # Successful XMC flash chip boot-up detected by RDID, skipping.
+
+ sfdp_mfid_addr = 0x10
+ mf_id = esp.read_spiflash_sfdp(sfdp_mfid_addr, 8)
+ if mf_id != XMC_VENDOR_ID: # Non-XMC chip detected by SFDP Read, skipping.
+ return
+
+ print(
+ "WARNING: XMC flash chip boot-up failure detected! "
+ "Running XMC25QHxxC startup flow"
+ )
+ esp.run_spiflash_command(0xB9) # Enter DPD
+ esp.run_spiflash_command(0x79) # Enter UDPD
+ esp.run_spiflash_command(0xFF) # Exit UDPD
+ time.sleep(0.002) # Delay tXUDPD
+ esp.run_spiflash_command(0xAB) # Release Power-Down
+ time.sleep(0.00002)
+ # Check for success
+ if not is_xmc_chip_strict():
+ print("WARNING: XMC flash boot-up fix failed.")
+ print("XMC flash chip boot-up fix successful!")
+
+ # Check flash chip connection
+ if not esp.secure_download_mode:
+ try:
+ flash_id = esp.flash_id()
+ if flash_id in (0xFFFFFF, 0x000000):
+ print(
+ "WARNING: Failed to communicate with the flash chip, "
+ "read/write operations will fail. "
+ "Try checking the chip connections or removing "
+ "any other hardware connected to IOs."
+ )
+ if (
+ hasattr(args, "spi_connection")
+ and args.spi_connection is not None
+ ):
+ print(
+ "Some GPIO pins might be used by other peripherals, "
+ "try using another --spi-connection combination."
+ )
+
+ except FatalError as e:
+ raise FatalError(f"Unable to verify flash chip connection ({e}).")
+
+ # Check if XMC SPI flash chip booted-up successfully, fix if not
+ if not esp.secure_download_mode:
+ try:
+ flash_xmc_startup()
+ except FatalError as e:
+ esp.trace(f"Unable to perform XMC flash chip startup sequence ({e}).")
+
+ if hasattr(args, "flash_size"):
+ print("Configuring flash size...")
+ if args.flash_size == "detect":
+ flash_size = detect_flash_size(esp, args)
+ elif args.flash_size == "keep":
+ flash_size = detect_flash_size(esp, args=None)
+ if not esp.IS_STUB:
+ print(
+ "WARNING: In case of failure, please set a specific --flash_size."
+ )
+ else:
+ flash_size = args.flash_size
+
+ if flash_size is not None: # Secure download mode
+ esp.flash_set_parameters(flash_size_bytes(flash_size))
+ # Check if stub supports chosen flash size
+ if (
+ esp.IS_STUB
+ and esp.CHIP_NAME != "ESP32-S3"
+ and flash_size_bytes(flash_size) > 16 * 1024 * 1024
+ ):
+ print(
+ "WARNING: Flasher stub doesn't fully support flash size larger "
+ "than 16MB, in case of failure use --no-stub."
+ )
+
+ if getattr(args, "size", "") == "all":
+ if esp.secure_download_mode:
+ raise FatalError(
+ "Detecting flash size is not supported in secure download mode. "
+ "Set an exact size value."
+ )
+ # detect flash size
+ flash_id = esp.flash_id()
+ size_id = flash_id >> 16
+ size_str = DETECTED_FLASH_SIZES.get(size_id)
+ if size_str is None:
+ raise FatalError(
+ "Detecting flash size failed. Set an exact size value."
+ )
+ print(f"Detected flash size: {size_str}")
+ args.size = flash_size_bytes(size_str)
+
+ if esp.IS_STUB and hasattr(args, "address") and hasattr(args, "size"):
+ if esp.CHIP_NAME != "ESP32-S3" and args.address + args.size > 0x1000000:
+ print(
+ "WARNING: Flasher stub doesn't fully support flash size larger "
+ "than 16MB, in case of failure use --no-stub."
+ )
+
+ try:
+ operation_func(esp, args)
+ finally:
+ try: # Clean up AddrFilenamePairAction files
+ for address, argfile in args.addr_filename:
+ argfile.close()
+ except AttributeError:
+ pass
+
+ # Handle post-operation behaviour (reset or other)
+ if operation_func == load_ram:
+ # the ESP is now running the loaded image, so let it run
+ print("Exiting immediately.")
+ elif args.after == "hard_reset":
+ esp.hard_reset()
+ elif args.after == "soft_reset":
+ print("Soft resetting...")
+ # flash_finish will trigger a soft reset
+ esp.soft_reset(False)
+ elif args.after == "no_reset_stub":
+ print("Staying in flasher stub.")
+ else: # args.after == 'no_reset'
+ print("Staying in bootloader.")
+ if esp.IS_STUB:
+ esp.soft_reset(True) # exit stub back to ROM loader
+
+ if not external_esp:
+ esp._port.close()
+
+ else:
+ operation_func(args)
+
+
+def arg_auto_int(x):
+ return int(x, 0)
+
+
+def arg_auto_size(x):
+ x = x.lower()
+ return x if x == "all" else arg_auto_int(x)
+
+
+def arg_auto_chunk_size(string: str) -> int:
+ num = int(string, 0)
+ if num & 3 != 0:
+ raise argparse.ArgumentTypeError("Chunk size should be a 4-byte aligned number")
+ return num
+
+
+def get_port_list(vids=[], pids=[], names=[]):
+ if list_ports is None:
+ raise FatalError(
+ "Listing all serial ports is currently not available. "
+ "Please try to specify the port when running esptool.py or update "
+ "the pyserial package to the latest version"
+ )
+ ports = []
+ for port in list_ports.comports():
+ if sys.platform == "darwin" and port.device.endswith(
+ ("Bluetooth-Incoming-Port", "wlan-debug")
+ ):
+ continue
+ if vids and (port.vid is None or port.vid not in vids):
+ continue
+ if pids and (port.pid is None or port.pid not in pids):
+ continue
+ if names and (
+ port.name is None or all(name not in port.name for name in names)
+ ):
+ continue
+ ports.append(port.device)
+ return sorted(ports)
+
+
+def expand_file_arguments(argv):
+ """
+ Any argument starting with "@" gets replaced with all values read from a text file.
+ Text file arguments can be split by newline or by space.
+ Values are added "as-is", as if they were specified in this order
+ on the command line.
+ """
+ new_args = []
+ expanded = False
+ for arg in argv:
+ if arg.startswith("@"):
+ expanded = True
+ with open(arg[1:], "r") as f:
+ for line in f.readlines():
+ new_args += shlex.split(line)
+ else:
+ new_args.append(arg)
+ if expanded:
+ print(f"esptool.py {' '.join(new_args)}")
+ return new_args
+ return argv
+
+
+def connect_loop(
+ port: str,
+ initial_baud: int,
+ chip: str,
+ max_retries: int,
+ trace: bool = False,
+ before: str = "default_reset",
+):
+ chip_class = CHIP_DEFS[chip]
+ esp = None
+ print(f"Serial port {port}")
+
+ first = True
+ ten_cycle = cycle(chain(repeat(False, 9), (True,)))
+ retry_loop = chain(
+ repeat(False, max_retries - 1), (True,) if max_retries else cycle((False,))
+ )
+
+ for last, every_tenth in zip(retry_loop, ten_cycle):
+ try:
+ esp = chip_class(port, initial_baud, trace)
+ if not first:
+ # break the retrying line
+ print("")
+ esp.connect(before)
+ return esp
+ except (
+ FatalError,
+ serial.serialutil.SerialException,
+ IOError,
+ OSError,
+ ) as err:
+ if esp and esp._port:
+ esp._port.close()
+ esp = None
+ if first:
+ print(err)
+ print("Retrying failed connection", end="", flush=True)
+ first = False
+ if last:
+ raise err
+ if every_tenth:
+ # print a dot every second
+ print(".", end="", flush=True)
+ time.sleep(0.1)
+
+
+def get_default_connected_device(
+ serial_list,
+ port,
+ connect_attempts,
+ initial_baud,
+ chip="auto",
+ trace=False,
+ before="default_reset",
+):
+ _esp = None
+ for each_port in reversed(serial_list):
+ print("Serial port %s" % each_port)
+ try:
+ if chip == "auto":
+ _esp = detect_chip(
+ each_port, initial_baud, before, trace, connect_attempts
+ )
+ else:
+ chip_class = CHIP_DEFS[chip]
+ _esp = chip_class(each_port, initial_baud, trace)
+ _esp.connect(before, connect_attempts)
+ break
+ except (FatalError, OSError) as err:
+ if port is not None:
+ raise
+ print("%s failed to connect: %s" % (each_port, err))
+ if _esp and _esp._port:
+ _esp._port.close()
+ _esp = None
+ return _esp
+
+
+class SpiConnectionAction(argparse.Action):
+ """
+ Custom action to parse 'spi connection' override.
+ Values are SPI, HSPI, or a sequence of 5 pin numbers separated by commas.
+ """
+
+ def __call__(self, parser, namespace, value, option_string=None):
+ if value.upper() in ["SPI", "HSPI"]:
+ values = value.upper()
+ elif "," in value:
+ values = value.split(",")
+ if len(values) != 5:
+ raise argparse.ArgumentError(
+ self,
+ f"{value} is not a valid list of comma-separate pin numbers. "
+ "Must be 5 numbers - CLK,Q,D,HD,CS.",
+ )
+ try:
+ values = tuple(int(v, 0) for v in values)
+ except ValueError:
+ raise argparse.ArgumentError(
+ self,
+ f"{values} is not a valid argument. "
+ "All pins must be numeric values",
+ )
+ else:
+ raise argparse.ArgumentError(
+ self,
+ f"{value} is not a valid spi-connection value. "
+ "Values are SPI, HSPI, or a sequence of 5 pin numbers - CLK,Q,D,HD,CS.",
+ )
+ setattr(namespace, self.dest, values)
+
+
+class AutoHex2BinAction(argparse.Action):
+ """Custom parser class for auto conversion of input files from hex to bin"""
+
+ def __call__(self, parser, namespace, value, option_string=None):
+ try:
+ with open(value, "rb") as f:
+ # if hex file was detected replace hex file with converted temp bin
+ # otherwise keep the original file
+ value = intel_hex_to_bin(f).name
+ except IOError as e:
+ raise argparse.ArgumentError(self, e)
+ setattr(namespace, self.dest, value)
+
+
+class AddrFilenamePairAction(argparse.Action):
+ """Custom parser class for the address/filename pairs passed as arguments"""
+
+ def __init__(self, option_strings, dest, nargs="+", **kwargs):
+ super(AddrFilenamePairAction, self).__init__(
+ option_strings, dest, nargs, **kwargs
+ )
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ # validate pair arguments
+ pairs = []
+ for i in range(0, len(values), 2):
+ try:
+ address = int(values[i], 0)
+ except ValueError:
+ raise argparse.ArgumentError(
+ self, 'Address "%s" must be a number' % values[i]
+ )
+ try:
+ argfile = open(values[i + 1], "rb")
+ except IOError as e:
+ raise argparse.ArgumentError(self, e)
+ except IndexError:
+ raise argparse.ArgumentError(
+ self,
+ "Must be pairs of an address "
+ "and the binary filename to write there",
+ )
+ # check for intel hex files and convert them to bin
+ argfile = intel_hex_to_bin(argfile, address)
+ pairs.append((address, argfile))
+
+ # Sort the addresses and check for overlapping
+ end = 0
+ for address, argfile in sorted(pairs, key=lambda x: x[0]):
+ argfile.seek(0, 2) # seek to end
+ size = argfile.tell()
+ argfile.seek(0)
+ sector_start = address & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)
+ sector_end = (
+ (address + size + ESPLoader.FLASH_SECTOR_SIZE - 1)
+ & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)
+ ) - 1
+ if sector_start < end:
+ message = "Detected overlap at address: 0x%x for file: %s" % (
+ address,
+ argfile.name,
+ )
+ raise argparse.ArgumentError(self, message)
+ end = sector_end
+ setattr(namespace, self.dest, pairs)
+
+
+def _main():
+ try:
+ main()
+ except FatalError as e:
+ print(f"\nA fatal error occurred: {e}")
+ sys.exit(2)
+ except serial.serialutil.SerialException as e:
+ print(f"\nA serial exception error occurred: {e}")
+ print(
+ "Note: This error originates from pySerial. "
+ "It is likely not a problem with esptool, "
+ "but with the hardware connection or drivers."
+ )
+ print(
+ "For troubleshooting steps visit: "
+ "https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html"
+ )
+ sys.exit(1)
+ except StopIteration:
+ print(traceback.format_exc())
+ print("A fatal error occurred: The chip stopped responding.")
+ sys.exit(2)
+
+
+if __name__ == "__main__":
+ _main()
diff --git a/mixly/tools/python/esptool/__main__.py b/mixly/tools/python/esptool/__main__.py
new file mode 100644
index 00000000..11e3bce1
--- /dev/null
+++ b/mixly/tools/python/esptool/__main__.py
@@ -0,0 +1,9 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import esptool
+
+if __name__ == "__main__":
+ esptool._main()
diff --git a/mixly/tools/python/esptool/bin_image.py b/mixly/tools/python/esptool/bin_image.py
new file mode 100644
index 00000000..a3a90dea
--- /dev/null
+++ b/mixly/tools/python/esptool/bin_image.py
@@ -0,0 +1,1380 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import binascii
+import copy
+import hashlib
+import io
+import os
+import re
+import struct
+import tempfile
+from typing import IO, Optional
+
+from intelhex import HexRecordError, IntelHex
+
+from .loader import ESPLoader
+from .targets import (
+ ESP32C2ROM,
+ ESP32C3ROM,
+ ESP32C5ROM,
+ ESP32C5BETA3ROM,
+ ESP32C6BETAROM,
+ ESP32C6ROM,
+ ESP32C61ROM,
+ ESP32H2BETA1ROM,
+ ESP32H2BETA2ROM,
+ ESP32H2ROM,
+ ESP32P4ROM,
+ ESP32ROM,
+ ESP32S2ROM,
+ ESP32S3BETA2ROM,
+ ESP32S3ROM,
+ ESP8266ROM,
+)
+from .util import FatalError, byte, pad_to
+
+
+def align_file_position(f, size):
+ """Align the position in the file to the next block of specified size"""
+ align = (size - 1) - (f.tell() % size)
+ f.seek(align, 1)
+
+
+def intel_hex_to_bin(file: IO[bytes], start_addr: Optional[int] = None) -> IO[bytes]:
+ """Convert IntelHex file to temp binary file with padding from start_addr
+ If hex file was detected return temp bin file object; input file otherwise"""
+ INTEL_HEX_MAGIC = b":"
+ magic = file.read(1)
+ file.seek(0)
+ try:
+ if magic == INTEL_HEX_MAGIC:
+ ih = IntelHex()
+ ih.loadhex(file.name)
+ file.close()
+ bin = tempfile.NamedTemporaryFile(suffix=".bin", delete=False)
+ ih.tobinfile(bin, start=start_addr)
+ return bin
+ else:
+ return file
+ except (HexRecordError, UnicodeDecodeError):
+ # file started with HEX magic but the rest was not according to the standard
+ return file
+
+
+def LoadFirmwareImage(chip, image_file):
+ """
+ Load a firmware image. Can be for any supported SoC.
+
+ ESP8266 images will be examined to determine if they are original ROM firmware
+ images (ESP8266ROMFirmwareImage) or "v2" OTA bootloader images.
+
+ Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1)
+ or ESP8266V2FirmwareImage (v2).
+ """
+
+ def select_image_class(f, chip):
+ chip = re.sub(r"[-()]", "", chip.lower())
+ if chip != "esp8266":
+ return {
+ "esp32": ESP32FirmwareImage,
+ "esp32s2": ESP32S2FirmwareImage,
+ "esp32s3beta2": ESP32S3BETA2FirmwareImage,
+ "esp32s3": ESP32S3FirmwareImage,
+ "esp32c3": ESP32C3FirmwareImage,
+ "esp32c6beta": ESP32C6BETAFirmwareImage,
+ "esp32h2beta1": ESP32H2BETA1FirmwareImage,
+ "esp32h2beta2": ESP32H2BETA2FirmwareImage,
+ "esp32c2": ESP32C2FirmwareImage,
+ "esp32c6": ESP32C6FirmwareImage,
+ "esp32c61": ESP32C61FirmwareImage,
+ "esp32c5": ESP32C5FirmwareImage,
+ "esp32c5beta3": ESP32C5BETA3FirmwareImage,
+ "esp32h2": ESP32H2FirmwareImage,
+ "esp32p4": ESP32P4FirmwareImage,
+ }[chip](f)
+ else: # Otherwise, ESP8266 so look at magic to determine the image type
+ magic = ord(f.read(1))
+ f.seek(0)
+ if magic == ESPLoader.ESP_IMAGE_MAGIC:
+ return ESP8266ROMFirmwareImage(f)
+ elif magic == ESP8266V2FirmwareImage.IMAGE_V2_MAGIC:
+ return ESP8266V2FirmwareImage(f)
+ else:
+ raise FatalError("Invalid image magic number: %d" % magic)
+
+ if isinstance(image_file, str):
+ with open(image_file, "rb") as f:
+ return select_image_class(f, chip)
+ return select_image_class(image_file, chip)
+
+
+class ImageSegment(object):
+ """Wrapper class for a segment in an ESP image
+ (very similar to a section in an ELFImage also)"""
+
+ def __init__(self, addr, data, file_offs=None, flags=0):
+ self.addr = addr
+ self.data = data
+ self.file_offs = file_offs
+ self.flags = flags
+ self.include_in_checksum = True
+ if self.addr != 0:
+ self.pad_to_alignment(
+ 4
+ ) # pad all "real" ImageSegments 4 byte aligned length
+
+ def copy_with_new_addr(self, new_addr):
+ """Return a new ImageSegment with same data, but mapped at
+ a new address."""
+ return ImageSegment(new_addr, self.data, 0)
+
+ def split_image(self, split_len):
+ """Return a new ImageSegment which splits "split_len" bytes
+ from the beginning of the data. Remaining bytes are kept in
+ this segment object (and the start address is adjusted to match.)"""
+ result = copy.copy(self)
+ result.data = self.data[:split_len]
+ self.data = self.data[split_len:]
+ self.addr += split_len
+ self.file_offs = None
+ result.file_offs = None
+ return result
+
+ def __repr__(self):
+ r = "len 0x%05x load 0x%08x" % (len(self.data), self.addr)
+ if self.file_offs is not None:
+ r += " file_offs 0x%08x" % (self.file_offs)
+ return r
+
+ def get_memory_type(self, image):
+ """
+ Return a list describing the memory type(s) that is covered by this
+ segment's start address.
+ """
+ return [
+ map_range[2]
+ for map_range in image.ROM_LOADER.MEMORY_MAP
+ if map_range[0] <= self.addr < map_range[1]
+ ]
+
+ def pad_to_alignment(self, alignment):
+ self.data = pad_to(self.data, alignment, b"\x00")
+
+
+class ELFSection(ImageSegment):
+ """Wrapper class for a section in an ELF image, has a section
+ name as well as the common properties of an ImageSegment."""
+
+ def __init__(self, name, addr, data, flags):
+ super(ELFSection, self).__init__(addr, data, flags=flags)
+ self.name = name.decode("utf-8")
+
+ def __repr__(self):
+ return "%s %s" % (self.name, super(ELFSection, self).__repr__())
+
+
+class BaseFirmwareImage(object):
+ SEG_HEADER_LEN = 8
+ SHA256_DIGEST_LEN = 32
+ ELF_FLAG_WRITE = 0x1
+ ELF_FLAG_READ = 0x2
+ ELF_FLAG_EXEC = 0x4
+
+ """ Base class with common firmware image functions """
+
+ def __init__(self):
+ self.segments = []
+ self.entrypoint = 0
+ self.elf_sha256 = None
+ self.elf_sha256_offset = 0
+ self.pad_to_size = 0
+
+ def load_common_header(self, load_file, expected_magic):
+ (
+ magic,
+ segments,
+ self.flash_mode,
+ self.flash_size_freq,
+ self.entrypoint,
+ ) = struct.unpack(" 16:
+ raise FatalError(
+ "Invalid segment count %d (max 16). "
+ "Usually this indicates a linker script problem." % len(self.segments)
+ )
+
+ def load_segment(self, f, is_irom_segment=False):
+ """Load the next segment from the image file"""
+ file_offs = f.tell()
+ (offset, size) = struct.unpack(" 0x40200000 or offset < 0x3FFE0000 or size > 65536:
+ print("WARNING: Suspicious segment 0x%x, length %d" % (offset, size))
+
+ def maybe_patch_segment_data(self, f, segment_data):
+ """
+ If SHA256 digest of the ELF file needs to be inserted into this segment, do so.
+ Returns segment data.
+ """
+ segment_len = len(segment_data)
+ file_pos = f.tell() # file_pos is position in the .bin file
+ if (
+ self.elf_sha256_offset >= file_pos
+ and self.elf_sha256_offset < file_pos + segment_len
+ ):
+ # SHA256 digest needs to be patched into this binary segment,
+ # calculate offset of the digest inside the binary segment.
+ patch_offset = self.elf_sha256_offset - file_pos
+ # Sanity checks
+ if (
+ patch_offset < self.SEG_HEADER_LEN
+ or patch_offset + self.SHA256_DIGEST_LEN > segment_len
+ ):
+ raise FatalError(
+ "Cannot place SHA256 digest on segment boundary"
+ "(elf_sha256_offset=%d, file_pos=%d, segment_size=%d)"
+ % (self.elf_sha256_offset, file_pos, segment_len)
+ )
+ # offset relative to the data part
+ patch_offset -= self.SEG_HEADER_LEN
+ if (
+ segment_data[patch_offset : patch_offset + self.SHA256_DIGEST_LEN]
+ != b"\x00" * self.SHA256_DIGEST_LEN
+ ):
+ raise FatalError(
+ "Contents of segment at SHA256 digest offset 0x%x are not all zero."
+ " Refusing to overwrite." % self.elf_sha256_offset
+ )
+ assert len(self.elf_sha256) == self.SHA256_DIGEST_LEN
+ segment_data = (
+ segment_data[0:patch_offset]
+ + self.elf_sha256
+ + segment_data[patch_offset + self.SHA256_DIGEST_LEN :]
+ )
+ return segment_data
+
+ def save_segment(self, f, segment, checksum=None):
+ """
+ Save the next segment to the image file,
+ return next checksum value if provided
+ """
+ segment_data = self.maybe_patch_segment_data(f, segment.data)
+ f.write(struct.pack(" 0:
+ if len(irom_segments) != 1:
+ raise FatalError(
+ "Found %d segments that could be irom0. Bad ELF file?"
+ % len(irom_segments)
+ )
+ return irom_segments[0]
+ return None
+
+ def get_non_irom_segments(self):
+ irom_segment = self.get_irom_segment()
+ return [s for s in self.segments if s != irom_segment]
+
+ def sort_segments(self):
+ if not self.segments:
+ return # nothing to sort
+ self.segments = sorted(self.segments, key=lambda s: s.addr)
+
+ def merge_adjacent_segments(self):
+ if not self.segments:
+ return # nothing to merge
+
+ segments = []
+ # The easiest way to merge the sections is the browse them backward.
+ for i in range(len(self.segments) - 1, 0, -1):
+ # elem is the previous section, the one `next_elem` may need to be
+ # merged in
+ elem = self.segments[i - 1]
+ next_elem = self.segments[i]
+ if all(
+ (
+ elem.get_memory_type(self) == next_elem.get_memory_type(self),
+ elem.include_in_checksum == next_elem.include_in_checksum,
+ next_elem.addr == elem.addr + len(elem.data),
+ next_elem.flags & self.ELF_FLAG_EXEC
+ == elem.flags & self.ELF_FLAG_EXEC,
+ )
+ ):
+ # Merge any segment that ends where the next one starts,
+ # without spanning memory types
+ #
+ # (don't 'pad' any gaps here as they may be excluded from the image
+ # due to 'noinit' or other reasons.)
+ elem.data += next_elem.data
+ else:
+ # The section next_elem cannot be merged into the previous one,
+ # which means it needs to be part of the final segments.
+ # As we are browsing the list backward, the elements need to be
+ # inserted at the beginning of the final list.
+ segments.insert(0, next_elem)
+
+ # The first segment will always be here as it cannot be merged into any
+ # "previous" section.
+ segments.insert(0, self.segments[0])
+
+ # note: we could sort segments here as well, but the ordering of segments is
+ # sometimes important for other reasons (like embedded ELF SHA-256),
+ # so we assume that the linker script will have produced any adjacent sections
+ # in linear order in the ELF, anyhow.
+ self.segments = segments
+
+ def set_mmu_page_size(self, size):
+ """
+ If supported, this should be overridden by the chip-specific class.
+ Gets called in elf2image.
+ """
+ print(
+ "WARNING: Changing MMU page size is not supported on {}! "
+ "Defaulting to 64KB.".format(self.ROM_LOADER.CHIP_NAME)
+ )
+
+
+class ESP8266ROMFirmwareImage(BaseFirmwareImage):
+ """'Version 1' firmware image, segments loaded directly by the ROM bootloader."""
+
+ ROM_LOADER = ESP8266ROM
+
+ def __init__(self, load_file=None):
+ super(ESP8266ROMFirmwareImage, self).__init__()
+ self.flash_mode = 0
+ self.flash_size_freq = 0
+ self.version = 1
+
+ if load_file is not None:
+ segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC)
+
+ for _ in range(segments):
+ self.load_segment(load_file)
+ self.checksum = self.read_checksum(load_file)
+
+ self.verify()
+
+ def default_output_name(self, input_file):
+ """Derive a default output name from the ELF name."""
+ return input_file + "-"
+
+ def save(self, basename):
+ """Save a set of V1 images for flashing. Parameter is a base filename."""
+ # IROM data goes in its own plain binary file
+ irom_segment = self.get_irom_segment()
+ if irom_segment is not None:
+ with open(
+ "%s0x%05x.bin"
+ % (basename, irom_segment.addr - ESP8266ROM.IROM_MAP_START),
+ "wb",
+ ) as f:
+ f.write(irom_segment.data)
+
+ # everything but IROM goes at 0x00000 in an image file
+ normal_segments = self.get_non_irom_segments()
+ with open("%s0x00000.bin" % basename, "wb") as f:
+ self.write_common_header(f, normal_segments)
+ checksum = ESPLoader.ESP_CHECKSUM_MAGIC
+ for segment in normal_segments:
+ checksum = self.save_segment(f, segment, checksum)
+ self.append_checksum(f, checksum)
+
+
+ESP8266ROM.BOOTLOADER_IMAGE = ESP8266ROMFirmwareImage
+
+
+class ESP8266V2FirmwareImage(BaseFirmwareImage):
+ """'Version 2' firmware image, segments loaded by software bootloader stub
+ (ie Espressif bootloader or rboot)
+ """
+
+ ROM_LOADER = ESP8266ROM
+ # First byte of the "v2" application image
+ IMAGE_V2_MAGIC = 0xEA
+
+ # First 'segment' value in a "v2" application image,
+ # appears to be a constant version value?
+ IMAGE_V2_SEGMENT = 4
+
+ def __init__(self, load_file=None):
+ super(ESP8266V2FirmwareImage, self).__init__()
+ self.version = 2
+ if load_file is not None:
+ segments = self.load_common_header(load_file, self.IMAGE_V2_MAGIC)
+ if segments != self.IMAGE_V2_SEGMENT:
+ # segment count is not really segment count here,
+ # but we expect to see '4'
+ print(
+ 'Warning: V2 header has unexpected "segment" count %d (usually 4)'
+ % segments
+ )
+
+ # irom segment comes before the second header
+ #
+ # the file is saved in the image with a zero load address
+ # in the header, so we need to calculate a load address
+ irom_segment = self.load_segment(load_file, True)
+ # for actual mapped addr, add ESP8266ROM.IROM_MAP_START + flashing_addr + 8
+ irom_segment.addr = 0
+ irom_segment.include_in_checksum = False
+
+ first_flash_mode = self.flash_mode
+ first_flash_size_freq = self.flash_size_freq
+ first_entrypoint = self.entrypoint
+ # load the second header
+
+ segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC)
+
+ if first_flash_mode != self.flash_mode:
+ print(
+ "WARNING: Flash mode value in first header (0x%02x) disagrees "
+ "with second (0x%02x). Using second value."
+ % (first_flash_mode, self.flash_mode)
+ )
+ if first_flash_size_freq != self.flash_size_freq:
+ print(
+ "WARNING: Flash size/freq value in first header (0x%02x) disagrees "
+ "with second (0x%02x). Using second value."
+ % (first_flash_size_freq, self.flash_size_freq)
+ )
+ if first_entrypoint != self.entrypoint:
+ print(
+ "WARNING: Entrypoint address in first header (0x%08x) disagrees "
+ "with second header (0x%08x). Using second value."
+ % (first_entrypoint, self.entrypoint)
+ )
+
+ # load all the usual segments
+ for _ in range(segments):
+ self.load_segment(load_file)
+ self.checksum = self.read_checksum(load_file)
+
+ self.verify()
+
+ def default_output_name(self, input_file):
+ """Derive a default output name from the ELF name."""
+ irom_segment = self.get_irom_segment()
+ if irom_segment is not None:
+ irom_offs = irom_segment.addr - ESP8266ROM.IROM_MAP_START
+ else:
+ irom_offs = 0
+ return "%s-0x%05x.bin" % (
+ os.path.splitext(input_file)[0],
+ irom_offs & ~(ESPLoader.FLASH_SECTOR_SIZE - 1),
+ )
+
+ def save(self, filename):
+ with open(filename, "wb") as f:
+ # Save first header for irom0 segment
+ f.write(
+ struct.pack(
+ b" 0:
+ last_addr = flash_segments[0].addr
+ for segment in flash_segments[1:]:
+ if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN:
+ raise FatalError(
+ "Segment loaded at 0x%08x lands in same 64KB flash mapping "
+ "as segment loaded at 0x%08x. Can't generate binary. "
+ "Suggest changing linker script or ELF to merge sections."
+ % (segment.addr, last_addr)
+ )
+ last_addr = segment.addr
+
+ def get_alignment_data_needed(segment):
+ # Actual alignment (in data bytes) required for a segment header:
+ # positioned so that after we write the next 8 byte header,
+ # file_offs % IROM_ALIGN == segment.addr % IROM_ALIGN
+ #
+ # (this is because the segment's vaddr may not be IROM_ALIGNed,
+ # more likely is aligned IROM_ALIGN+0x18
+ # to account for the binary file header
+ align_past = (segment.addr % self.IROM_ALIGN) - self.SEG_HEADER_LEN
+ pad_len = (self.IROM_ALIGN - (f.tell() % self.IROM_ALIGN)) + align_past
+ if pad_len == 0 or pad_len == self.IROM_ALIGN:
+ return 0 # already aligned
+
+ # subtract SEG_HEADER_LEN a second time,
+ # as the padding block has a header as well
+ pad_len -= self.SEG_HEADER_LEN
+ if pad_len < 0:
+ pad_len += self.IROM_ALIGN
+ return pad_len
+
+ if self.ram_only_header:
+ # write RAM segments first in order to get only RAM segments quantity
+ # and checksum (ROM bootloader will only care for RAM segments and its
+ # correct checksums)
+ for segment in ram_segments:
+ checksum = self.save_segment(f, segment, checksum)
+ total_segments += 1
+ self.append_checksum(f, checksum)
+
+ # reversing to match the same section order from linker script
+ flash_segments.reverse()
+ for segment in flash_segments:
+ pad_len = get_alignment_data_needed(segment)
+ # Some chips have a non-zero load offset (eg. 0x1000)
+ # therefore we shift the ROM segments "-load_offset"
+ # so it will be aligned properly after it is flashed
+ align_min = (
+ self.ROM_LOADER.BOOTLOADER_FLASH_OFFSET - self.SEG_HEADER_LEN
+ )
+ if pad_len < align_min:
+ # in case pad_len does not fit minimum alignment,
+ # pad it to next aligned boundary
+ pad_len += self.IROM_ALIGN
+
+ pad_len -= self.ROM_LOADER.BOOTLOADER_FLASH_OFFSET
+ pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell())
+ self.save_segment(f, pad_segment)
+ total_segments += 1
+ # check the alignment
+ assert (f.tell() + 8 + self.ROM_LOADER.BOOTLOADER_FLASH_OFFSET) % (
+ self.IROM_ALIGN
+ ) == segment.addr % self.IROM_ALIGN
+ # save the flash segment but not saving its checksum neither
+ # saving the number of flash segments, since ROM bootloader
+ # should "not see" them
+ self.save_flash_segment(f, segment)
+ total_segments += 1
+ else: # not self.ram_only_header
+ # try to fit each flash segment on a 64kB aligned boundary
+ # by padding with parts of the non-flash segments...
+ while len(flash_segments) > 0:
+ segment = flash_segments[0]
+ pad_len = get_alignment_data_needed(segment)
+ if pad_len > 0: # need to pad
+ if len(ram_segments) > 0 and pad_len > self.SEG_HEADER_LEN:
+ pad_segment = ram_segments[0].split_image(pad_len)
+ if len(ram_segments[0].data) == 0:
+ ram_segments.pop(0)
+ else:
+ pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell())
+ checksum = self.save_segment(f, pad_segment, checksum)
+ total_segments += 1
+ else:
+ # write the flash segment
+ assert (
+ f.tell() + 8
+ ) % self.IROM_ALIGN == segment.addr % self.IROM_ALIGN
+ checksum = self.save_flash_segment(f, segment, checksum)
+ flash_segments.pop(0)
+ total_segments += 1
+
+ # flash segments all written, so write any remaining RAM segments
+ for segment in ram_segments:
+ checksum = self.save_segment(f, segment, checksum)
+ total_segments += 1
+
+ if self.secure_pad:
+ # pad the image so that after signing it will end on a a 64KB boundary.
+ # This ensures all mapped flash content will be verified.
+ if not self.append_digest:
+ raise FatalError(
+ "secure_pad only applies if a SHA-256 digest "
+ "is also appended to the image"
+ )
+ align_past = (f.tell() + self.SEG_HEADER_LEN) % self.IROM_ALIGN
+ # 16 byte aligned checksum
+ # (force the alignment to simplify calculations)
+ checksum_space = 16
+ if self.secure_pad == "1":
+ # after checksum: SHA-256 digest +
+ # (to be added by signing process) version,
+ # signature + 12 trailing bytes due to alignment
+ space_after_checksum = 32 + 4 + 64 + 12
+ elif self.secure_pad == "2": # Secure Boot V2
+ # after checksum: SHA-256 digest +
+ # signature sector,
+ # but we place signature sector after the 64KB boundary
+ space_after_checksum = 32
+ pad_len = (
+ self.IROM_ALIGN - align_past - checksum_space - space_after_checksum
+ ) % self.IROM_ALIGN
+ pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell())
+
+ checksum = self.save_segment(f, pad_segment, checksum)
+ total_segments += 1
+
+ if not self.ram_only_header:
+ # done writing segments
+ self.append_checksum(f, checksum)
+ image_length = f.tell()
+
+ if self.secure_pad:
+ assert ((image_length + space_after_checksum) % self.IROM_ALIGN) == 0
+
+ # kinda hacky: go back to the initial header and write the new segment count
+ # that includes padding segments. This header is not checksummed
+ f.seek(1)
+ if self.ram_only_header:
+ # Update the header with the RAM segments quantity as it should be
+ # visible by the ROM bootloader
+ f.write(bytes([len(ram_segments)]))
+ else:
+ f.write(bytes([total_segments]))
+
+ if self.append_digest:
+ # calculate the SHA256 of the whole file and append it
+ f.seek(0)
+ digest = hashlib.sha256()
+ digest.update(f.read(image_length))
+ f.write(digest.digest())
+
+ if self.pad_to_size:
+ image_length = f.tell()
+ if image_length % self.pad_to_size != 0:
+ pad_by = self.pad_to_size - (image_length % self.pad_to_size)
+ f.write(b"\xff" * pad_by)
+
+ with open(filename, "wb") as real_file:
+ real_file.write(f.getvalue())
+
+ def load_extended_header(self, load_file):
+ def split_byte(n):
+ return (n & 0x0F, (n >> 4) & 0x0F)
+
+ fields = list(
+ struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))
+ )
+
+ self.wp_pin = fields[0]
+
+ # SPI pin drive stengths are two per byte
+ self.clk_drv, self.q_drv = split_byte(fields[1])
+ self.d_drv, self.cs_drv = split_byte(fields[2])
+ self.hd_drv, self.wp_drv = split_byte(fields[3])
+
+ self.chip_id = fields[4]
+ if self.chip_id != self.ROM_LOADER.IMAGE_CHIP_ID:
+ print(
+ (
+ "Unexpected chip id in image. Expected %d but value was %d. "
+ "Is this image for a different chip model?"
+ )
+ % (self.ROM_LOADER.IMAGE_CHIP_ID, self.chip_id)
+ )
+
+ self.min_rev = fields[5]
+ self.min_rev_full = fields[6]
+ self.max_rev_full = fields[7]
+
+ append_digest = fields[-1] # last byte is append_digest
+ if append_digest in [0, 1]:
+ self.append_digest = append_digest == 1
+ else:
+ raise RuntimeError(
+ "Invalid value for append_digest field (0x%02x). Should be 0 or 1.",
+ append_digest,
+ )
+
+ def save_extended_header(self, save_file):
+ def join_byte(ln, hn):
+ return (ln & 0x0F) + ((hn & 0x0F) << 4)
+
+ append_digest = 1 if self.append_digest else 0
+
+ fields = [
+ self.wp_pin,
+ join_byte(self.clk_drv, self.q_drv),
+ join_byte(self.d_drv, self.cs_drv),
+ join_byte(self.hd_drv, self.wp_drv),
+ self.ROM_LOADER.IMAGE_CHIP_ID,
+ self.min_rev,
+ self.min_rev_full,
+ self.max_rev_full,
+ ]
+ fields += [0] * 4 # padding
+ fields += [append_digest]
+
+ packed = struct.pack(self.EXTENDED_HEADER_STRUCT_FMT, *fields)
+ save_file.write(packed)
+
+
+class ESP8266V3FirmwareImage(ESP32FirmwareImage):
+ """ESP8266 V3 firmware image is very similar to ESP32 image"""
+
+ EXTENDED_HEADER_STRUCT_FMT = "B" * 16
+
+ def is_flash_addr(self, addr):
+ return addr > ESP8266ROM.IROM_MAP_START
+
+ def save(self, filename):
+ total_segments = 0
+ with io.BytesIO() as f: # write file to memory first
+ self.write_common_header(f, self.segments)
+
+ checksum = ESPLoader.ESP_CHECKSUM_MAGIC
+
+ # split segments into flash-mapped vs ram-loaded,
+ # and take copies so we can mutate them
+ flash_segments = [
+ copy.deepcopy(s)
+ for s in sorted(self.segments, key=lambda s: s.addr)
+ if self.is_flash_addr(s.addr) and len(s.data)
+ ]
+ ram_segments = [
+ copy.deepcopy(s)
+ for s in sorted(self.segments, key=lambda s: s.addr)
+ if not self.is_flash_addr(s.addr) and len(s.data)
+ ]
+
+ # check for multiple ELF sections that are mapped in the same
+ # flash mapping region. This is usually a sign of a broken linker script,
+ # but if you have a legitimate use case then let us know
+ if len(flash_segments) > 0:
+ last_addr = flash_segments[0].addr
+ for segment in flash_segments[1:]:
+ if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN:
+ raise FatalError(
+ "Segment loaded at 0x%08x lands in same 64KB flash mapping "
+ "as segment loaded at 0x%08x. Can't generate binary. "
+ "Suggest changing linker script or ELF to merge sections."
+ % (segment.addr, last_addr)
+ )
+ last_addr = segment.addr
+
+ # try to fit each flash segment on a 64kB aligned boundary
+ # by padding with parts of the non-flash segments...
+ while len(flash_segments) > 0:
+ segment = flash_segments[0]
+ # remove 8 bytes empty data for insert segment header
+ if isinstance(segment, ELFSection) and segment.name == ".flash.rodata":
+ segment.data = segment.data[8:]
+ # write the flash segment
+ checksum = self.save_segment(f, segment, checksum)
+ flash_segments.pop(0)
+ total_segments += 1
+
+ # flash segments all written, so write any remaining RAM segments
+ for segment in ram_segments:
+ checksum = self.save_segment(f, segment, checksum)
+ total_segments += 1
+
+ # done writing segments
+ self.append_checksum(f, checksum)
+ image_length = f.tell()
+
+ # kinda hacky: go back to the initial header and write the new segment count
+ # that includes padding segments. This header is not checksummed
+ f.seek(1)
+ f.write(bytes([total_segments]))
+
+ if self.append_digest:
+ # calculate the SHA256 of the whole file and append it
+ f.seek(0)
+ digest = hashlib.sha256()
+ digest.update(f.read(image_length))
+ f.write(digest.digest())
+
+ with open(filename, "wb") as real_file:
+ real_file.write(f.getvalue())
+
+ def load_extended_header(self, load_file):
+ def split_byte(n):
+ return (n & 0x0F, (n >> 4) & 0x0F)
+
+ fields = list(
+ struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))
+ )
+
+ self.wp_pin = fields[0]
+
+ # SPI pin drive stengths are two per byte
+ self.clk_drv, self.q_drv = split_byte(fields[1])
+ self.d_drv, self.cs_drv = split_byte(fields[2])
+ self.hd_drv, self.wp_drv = split_byte(fields[3])
+
+ if fields[15] in [0, 1]:
+ self.append_digest = fields[15] == 1
+ else:
+ raise RuntimeError(
+ "Invalid value for append_digest field (0x%02x). Should be 0 or 1.",
+ fields[15],
+ )
+
+ # remaining fields in the middle should all be zero
+ if any(f for f in fields[4:15] if f != 0):
+ print(
+ "Warning: some reserved header fields have non-zero values. "
+ "This image may be from a newer esptool.py?"
+ )
+
+
+ESP32ROM.BOOTLOADER_IMAGE = ESP32FirmwareImage
+
+
+class ESP32S2FirmwareImage(ESP32FirmwareImage):
+ """ESP32S2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32S2ROM
+
+
+ESP32S2ROM.BOOTLOADER_IMAGE = ESP32S2FirmwareImage
+
+
+class ESP32S3BETA2FirmwareImage(ESP32FirmwareImage):
+ """ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32S3BETA2ROM
+
+
+ESP32S3BETA2ROM.BOOTLOADER_IMAGE = ESP32S3BETA2FirmwareImage
+
+
+class ESP32S3FirmwareImage(ESP32FirmwareImage):
+ """ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32S3ROM
+
+
+ESP32S3ROM.BOOTLOADER_IMAGE = ESP32S3FirmwareImage
+
+
+class ESP32C3FirmwareImage(ESP32FirmwareImage):
+ """ESP32C3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C3ROM
+
+
+ESP32C3ROM.BOOTLOADER_IMAGE = ESP32C3FirmwareImage
+
+
+class ESP32C6BETAFirmwareImage(ESP32FirmwareImage):
+ """ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C6BETAROM
+
+
+ESP32C6BETAROM.BOOTLOADER_IMAGE = ESP32C6BETAFirmwareImage
+
+
+class ESP32H2BETA1FirmwareImage(ESP32FirmwareImage):
+ """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32H2BETA1ROM
+
+
+ESP32H2BETA1ROM.BOOTLOADER_IMAGE = ESP32H2BETA1FirmwareImage
+
+
+class ESP32H2BETA2FirmwareImage(ESP32FirmwareImage):
+ """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32H2BETA2ROM
+
+
+ESP32H2BETA2ROM.BOOTLOADER_IMAGE = ESP32H2BETA2FirmwareImage
+
+
+class ESP32C2FirmwareImage(ESP32FirmwareImage):
+ """ESP32C2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C2ROM
+
+ def set_mmu_page_size(self, size):
+ if size not in [16384, 32768, 65536]:
+ raise FatalError(
+ "{} bytes is not a valid ESP32-C2 page size, "
+ "select from 64KB, 32KB, 16KB.".format(size)
+ )
+ self.IROM_ALIGN = size
+
+
+ESP32C2ROM.BOOTLOADER_IMAGE = ESP32C2FirmwareImage
+
+
+class ESP32C6FirmwareImage(ESP32FirmwareImage):
+ """ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C6ROM
+
+ def set_mmu_page_size(self, size):
+ if size not in [8192, 16384, 32768, 65536]:
+ raise FatalError(
+ "{} bytes is not a valid ESP32-C6 page size, "
+ "select from 64KB, 32KB, 16KB, 8KB.".format(size)
+ )
+ self.IROM_ALIGN = size
+
+
+ESP32C6ROM.BOOTLOADER_IMAGE = ESP32C6FirmwareImage
+
+
+class ESP32C61FirmwareImage(ESP32C6FirmwareImage):
+ """ESP32C61 Firmware Image almost exactly the same as ESP32C6FirmwareImage"""
+
+ ROM_LOADER = ESP32C61ROM
+
+
+ESP32C61ROM.BOOTLOADER_IMAGE = ESP32C61FirmwareImage
+
+
+class ESP32C5FirmwareImage(ESP32C6FirmwareImage):
+ """ESP32C5 Firmware Image almost exactly the same as ESP32C6FirmwareImage"""
+
+ ROM_LOADER = ESP32C5ROM
+
+
+ESP32C5ROM.BOOTLOADER_IMAGE = ESP32C5FirmwareImage
+
+
+class ESP32C5BETA3FirmwareImage(ESP32C6FirmwareImage):
+ """ESP32C5BETA3 Firmware Image almost exactly the same as ESP32C6FirmwareImage"""
+
+ ROM_LOADER = ESP32C5BETA3ROM
+
+
+ESP32C5BETA3ROM.BOOTLOADER_IMAGE = ESP32C5BETA3FirmwareImage
+
+
+class ESP32P4FirmwareImage(ESP32FirmwareImage):
+ """ESP32P4 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32P4ROM
+
+
+ESP32P4ROM.BOOTLOADER_IMAGE = ESP32P4FirmwareImage
+
+
+class ESP32H2FirmwareImage(ESP32C6FirmwareImage):
+ """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32H2ROM
+
+
+ESP32H2ROM.BOOTLOADER_IMAGE = ESP32H2FirmwareImage
+
+
+class ELFFile(object):
+ SEC_TYPE_PROGBITS = 0x01
+ SEC_TYPE_STRTAB = 0x03
+ SEC_TYPE_NOBITS = 0x08 # e.g. .bss section
+ SEC_TYPE_INITARRAY = 0x0E
+ SEC_TYPE_FINIARRAY = 0x0F
+
+ PROG_SEC_TYPES = (SEC_TYPE_PROGBITS, SEC_TYPE_INITARRAY, SEC_TYPE_FINIARRAY)
+
+ LEN_SEC_HEADER = 0x28
+
+ SEG_TYPE_LOAD = 0x01
+ LEN_SEG_HEADER = 0x20
+
+ def __init__(self, name):
+ # Load sections from the ELF file
+ self.name = name
+ with open(self.name, "rb") as f:
+ self._read_elf_file(f)
+
+ def get_section(self, section_name):
+ for s in self.sections:
+ if s.name == section_name:
+ return s
+ raise ValueError("No section %s in ELF file" % section_name)
+
+ def _read_elf_file(self, f):
+ # read the ELF file header
+ LEN_FILE_HEADER = 0x34
+ try:
+ (
+ ident,
+ _type,
+ machine,
+ _version,
+ self.entrypoint,
+ _phoff,
+ shoff,
+ _flags,
+ _ehsize,
+ _phentsize,
+ _phnum,
+ shentsize,
+ shnum,
+ shstrndx,
+ ) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER))
+ except struct.error as e:
+ raise FatalError(
+ "Failed to read a valid ELF header from %s: %s" % (self.name, e)
+ )
+
+ if byte(ident, 0) != 0x7F or ident[1:4] != b"ELF":
+ raise FatalError("%s has invalid ELF magic header" % self.name)
+ if machine not in [0x5E, 0xF3]:
+ raise FatalError(
+ "%s does not appear to be an Xtensa or an RISCV ELF file. "
+ "e_machine=%04x" % (self.name, machine)
+ )
+ if shentsize != self.LEN_SEC_HEADER:
+ raise FatalError(
+ "%s has unexpected section header entry size 0x%x (not 0x%x)"
+ % (self.name, shentsize, self.LEN_SEC_HEADER)
+ )
+ if shnum == 0:
+ raise FatalError("%s has 0 section headers" % (self.name))
+ self._read_sections(f, shoff, shnum, shstrndx)
+ self._read_segments(f, _phoff, _phnum, shstrndx)
+
+ def _read_sections(self, f, section_header_offs, section_header_count, shstrndx):
+ f.seek(section_header_offs)
+ len_bytes = section_header_count * self.LEN_SEC_HEADER
+ section_header = f.read(len_bytes)
+ if len(section_header) == 0:
+ raise FatalError(
+ "No section header found at offset %04x in ELF file."
+ % section_header_offs
+ )
+ if len(section_header) != (len_bytes):
+ raise FatalError(
+ "Only read 0x%x bytes from section header (expected 0x%x.) "
+ "Truncated ELF file?" % (len(section_header), len_bytes)
+ )
+
+ # walk through the section header and extract all sections
+ section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER)
+
+ def read_section_header(offs):
+ name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from(
+ " 0
+ ]
+ self.sections = prog_sections
+ self.nobits_sections = [
+ ELFSection(lookup_string(n_offs), lma, b"", flags=_flags)
+ for (n_offs, _type, lma, size, offs, _flags) in nobits_secitons
+ if lma != 0 and size > 0
+ ]
+
+ def _read_segments(self, f, segment_header_offs, segment_header_count, shstrndx):
+ f.seek(segment_header_offs)
+ len_bytes = segment_header_count * self.LEN_SEG_HEADER
+ segment_header = f.read(len_bytes)
+ if len(segment_header) == 0:
+ raise FatalError(
+ "No segment header found at offset %04x in ELF file."
+ % segment_header_offs
+ )
+ if len(segment_header) != (len_bytes):
+ raise FatalError(
+ "Only read 0x%x bytes from segment header (expected 0x%x.) "
+ "Truncated ELF file?" % (len(segment_header), len_bytes)
+ )
+
+ # walk through the segment header and extract all segments
+ segment_header_offsets = range(0, len(segment_header), self.LEN_SEG_HEADER)
+
+ def read_segment_header(offs):
+ (
+ seg_type,
+ seg_offs,
+ _vaddr,
+ lma,
+ size,
+ _memsize,
+ _flags,
+ _align,
+ ) = struct.unpack_from(" 0
+ ]
+ self.segments = prog_segments
+
+ def sha256(self):
+ # return SHA256 hash of the input ELF file
+ sha256 = hashlib.sha256()
+ with open(self.name, "rb") as f:
+ sha256.update(f.read())
+ return sha256.digest()
diff --git a/mixly/tools/python/esptool/cmds.py b/mixly/tools/python/esptool/cmds.py
new file mode 100644
index 00000000..4948c989
--- /dev/null
+++ b/mixly/tools/python/esptool/cmds.py
@@ -0,0 +1,1474 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import hashlib
+import io
+import os
+import struct
+import sys
+import time
+import zlib
+import itertools
+
+from intelhex import IntelHex
+from serial import SerialException
+
+from .bin_image import ELFFile, ImageSegment, LoadFirmwareImage
+from .bin_image import (
+ ESP8266ROMFirmwareImage,
+ ESP8266V2FirmwareImage,
+ ESP8266V3FirmwareImage,
+)
+from .loader import (
+ DEFAULT_CONNECT_ATTEMPTS,
+ DEFAULT_TIMEOUT,
+ ERASE_WRITE_TIMEOUT_PER_MB,
+ ESPLoader,
+ timeout_per_mb,
+)
+from .targets import CHIP_DEFS, CHIP_LIST, ROM_LIST
+from .uf2_writer import UF2Writer
+from .util import (
+ FatalError,
+ NotImplementedInROMError,
+ NotSupportedError,
+ UnsupportedCommandError,
+)
+from .util import (
+ div_roundup,
+ flash_size_bytes,
+ get_file_size,
+ hexify,
+ pad_to,
+ print_overwrite,
+)
+
+DETECTED_FLASH_SIZES = {
+ 0x12: "256KB",
+ 0x13: "512KB",
+ 0x14: "1MB",
+ 0x15: "2MB",
+ 0x16: "4MB",
+ 0x17: "8MB",
+ 0x18: "16MB",
+ 0x19: "32MB",
+ 0x1A: "64MB",
+ 0x1B: "128MB",
+ 0x1C: "256MB",
+ 0x20: "64MB",
+ 0x21: "128MB",
+ 0x22: "256MB",
+ 0x32: "256KB",
+ 0x33: "512KB",
+ 0x34: "1MB",
+ 0x35: "2MB",
+ 0x36: "4MB",
+ 0x37: "8MB",
+ 0x38: "16MB",
+ 0x39: "32MB",
+ 0x3A: "64MB",
+}
+
+FLASH_MODES = {"qio": 0, "qout": 1, "dio": 2, "dout": 3}
+
+
+def detect_chip(
+ port=ESPLoader.DEFAULT_PORT,
+ baud=ESPLoader.ESP_ROM_BAUD,
+ connect_mode="default_reset",
+ trace_enabled=False,
+ connect_attempts=DEFAULT_CONNECT_ATTEMPTS,
+):
+ """Use serial access to detect the chip type.
+
+ First, get_security_info command is sent to detect the ID of the chip
+ (supported only by ESP32-C3 and later, works even in the Secure Download Mode).
+ If this fails, we reconnect and fall-back to reading the magic number.
+ It's mapped at a specific ROM address and has a different value on each chip model.
+ This way we use one memory read and compare it to the magic number for each chip.
+
+ This routine automatically performs ESPLoader.connect() (passing
+ connect_mode parameter) as part of querying the chip.
+ """
+ inst = None
+ detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled)
+ if detect_port.serial_port.startswith("rfc2217:"):
+ detect_port.USES_RFC2217 = True
+ detect_port.connect(connect_mode, connect_attempts, detecting=True)
+ try:
+ print("Detecting chip type...", end="")
+ chip_id = detect_port.get_chip_id()
+ for cls in [
+ n for n in ROM_LIST if n.CHIP_NAME not in ("ESP8266", "ESP32", "ESP32-S2")
+ ]:
+ # cmd not supported on ESP8266 and ESP32 + ESP32-S2 doesn't return chip_id
+ if chip_id == cls.IMAGE_CHIP_ID:
+ inst = cls(detect_port._port, baud, trace_enabled=trace_enabled)
+ try:
+ inst.read_reg(
+ ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR
+ ) # Dummy read to check Secure Download mode
+ except UnsupportedCommandError:
+ inst.secure_download_mode = True
+ inst._post_connect()
+ break
+ else:
+ err_msg = f"Unexpected chip ID value {chip_id}."
+ except (UnsupportedCommandError, struct.error, FatalError) as e:
+ # UnsupportedCommandError: ESP8266/ESP32 ROM
+ # struct.error: ESP32-S2
+ # FatalError: ESP8266/ESP32 STUB
+ print(" Unsupported detection protocol, switching and trying again...")
+ try:
+ # ESP32/ESP8266 are reset after an unsupported command, need to reconnect
+ # (not needed on ESP32-S2)
+ if not isinstance(e, struct.error):
+ detect_port.connect(
+ connect_mode, connect_attempts, detecting=True, warnings=False
+ )
+ print("Detecting chip type...", end="")
+ sys.stdout.flush()
+ chip_magic_value = detect_port.read_reg(
+ ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR
+ )
+
+ for cls in ROM_LIST:
+ if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE:
+ inst = cls(detect_port._port, baud, trace_enabled=trace_enabled)
+ inst._post_connect()
+ inst.check_chip_id()
+ break
+ else:
+ err_msg = f"Unexpected chip magic value {chip_magic_value:#010x}."
+ except UnsupportedCommandError:
+ raise FatalError(
+ "Unsupported Command Error received. "
+ "Probably this means Secure Download Mode is enabled, "
+ "autodetection will not work. Need to manually specify the chip."
+ )
+ finally:
+ if inst is not None:
+ print(" %s" % inst.CHIP_NAME, end="")
+ if detect_port.sync_stub_detected:
+ inst = inst.STUB_CLASS(inst)
+ inst.sync_stub_detected = True
+ print("") # end line
+ return inst
+ raise FatalError(
+ f"{err_msg} Failed to autodetect chip type."
+ "\nProbably it is unsupported by this version of esptool."
+ )
+
+
+# "Operation" commands, executable at command line. One function each
+#
+# Each function takes either two args (, ) or a single
+# argument.
+
+
+def load_ram(esp, args):
+ image = LoadFirmwareImage(esp.CHIP_NAME, args.filename)
+
+ print("RAM boot...")
+ for seg in image.segments:
+ size = len(seg.data)
+ print("Downloading %d bytes at %08x..." % (size, seg.addr), end=" ")
+ sys.stdout.flush()
+ esp.mem_begin(
+ size, div_roundup(size, esp.ESP_RAM_BLOCK), esp.ESP_RAM_BLOCK, seg.addr
+ )
+
+ seq = 0
+ while len(seg.data) > 0:
+ esp.mem_block(seg.data[0 : esp.ESP_RAM_BLOCK], seq)
+ seg.data = seg.data[esp.ESP_RAM_BLOCK :]
+ seq += 1
+ print("done!")
+
+ print("All segments done, executing at %08x" % image.entrypoint)
+ esp.mem_finish(image.entrypoint)
+
+
+def read_mem(esp, args):
+ print("0x%08x = 0x%08x" % (args.address, esp.read_reg(args.address)))
+
+
+def write_mem(esp, args):
+ esp.write_reg(args.address, args.value, args.mask, 0)
+ print("Wrote %08x, mask %08x to %08x" % (args.value, args.mask, args.address))
+
+
+def dump_mem(esp, args):
+ with open(args.filename, "wb") as f:
+ for i in range(args.size // 4):
+ d = esp.read_reg(args.address + (i * 4))
+ f.write(struct.pack(b"> 16
+ flash_size = DETECTED_FLASH_SIZES.get(size_id)
+ if args is not None and args.flash_size == "detect":
+ if flash_size is None:
+ flash_size = "4MB"
+ print(
+ "WARNING: Could not auto-detect Flash size "
+ f"(FlashID={flash_id:#x}, SizeID={size_id:#x}), defaulting to 4MB"
+ )
+ else:
+ print("Auto-detected Flash size:", flash_size)
+ args.flash_size = flash_size
+ return flash_size
+
+
+def _update_image_flash_params(esp, address, args, image):
+ """
+ Modify the flash mode & size bytes if this looks like an executable bootloader image
+ """
+ if len(image) < 8:
+ return image # not long enough to be a bootloader image
+
+ # unpack the (potential) image header
+ magic, _, flash_mode, flash_size_freq = struct.unpack("BBBB", image[:4])
+ if address != esp.BOOTLOADER_FLASH_OFFSET:
+ return image # not flashing bootloader offset, so don't modify this
+
+ if (args.flash_mode, args.flash_freq, args.flash_size) == ("keep",) * 3:
+ return image # all settings are 'keep', not modifying anything
+
+ # easy check if this is an image: does it start with a magic byte?
+ if magic != esp.ESP_IMAGE_MAGIC:
+ print(
+ "Warning: Image file at 0x%x doesn't look like an image file, "
+ "so not changing any flash settings." % address
+ )
+ return image
+
+ # make sure this really is an image, and not just data that
+ # starts with esp.ESP_IMAGE_MAGIC (mostly a problem for encrypted
+ # images that happen to start with a magic byte
+ try:
+ test_image = esp.BOOTLOADER_IMAGE(io.BytesIO(image))
+ test_image.verify()
+ except Exception:
+ print(
+ "Warning: Image file at 0x%x is not a valid %s image, "
+ "so not changing any flash settings." % (address, esp.CHIP_NAME)
+ )
+ return image
+
+ # After the 8-byte header comes the extended header for chips others than ESP8266.
+ # The 15th byte of the extended header indicates if the image is protected by
+ # a SHA256 checksum. In that case we recalculate the SHA digest after modifying the header.
+ sha_appended = args.chip != "esp8266" and image[8 + 15] == 1
+
+ if args.flash_mode != "keep":
+ flash_mode = FLASH_MODES[args.flash_mode]
+
+ flash_freq = flash_size_freq & 0x0F
+ if args.flash_freq != "keep":
+ flash_freq = esp.parse_flash_freq_arg(args.flash_freq)
+
+ flash_size = flash_size_freq & 0xF0
+ if args.flash_size != "keep":
+ flash_size = esp.parse_flash_size_arg(args.flash_size)
+
+ flash_params = struct.pack(b"BB", flash_mode, flash_size + flash_freq)
+ if flash_params != image[2:4]:
+ print("Flash params set to 0x%04x" % struct.unpack(">H", flash_params))
+ image = image[0:2] + flash_params + image[4:]
+
+ # recalculate the SHA digest if it was appended
+ if sha_appended:
+ # Since the changes are only made for images located in the bootloader offset,
+ # we can assume that the image is always a bootloader image.
+ # For merged binaries, we check the bootloader SHA when parameters are changed.
+ image_object = esp.BOOTLOADER_IMAGE(io.BytesIO(image))
+ # get the image header, extended header (if present) and data
+ image_data_before_sha = image[: image_object.data_length]
+ # get the image data after the SHA digest (primary for merged binaries)
+ image_data_after_sha = image[
+ (image_object.data_length + image_object.SHA256_DIGEST_LEN) :
+ ]
+
+ sha_digest_calculated = hashlib.sha256(image_data_before_sha).digest()
+ image = bytes(
+ itertools.chain(
+ image_data_before_sha, sha_digest_calculated, image_data_after_sha
+ )
+ )
+
+ # get the SHA digest newly stored in the image and compare it to the calculated one
+ image_stored_sha = image[
+ image_object.data_length : image_object.data_length
+ + image_object.SHA256_DIGEST_LEN
+ ]
+
+ if hexify(sha_digest_calculated) == hexify(image_stored_sha):
+ print("SHA digest in image updated")
+ else:
+ print(
+ "WARNING: SHA recalculation for binary failed!\n"
+ f"\tExpected calculated SHA: {hexify(sha_digest_calculated)}\n"
+ f"\tSHA stored in binary: {hexify(image_stored_sha)}"
+ )
+
+ return image
+
+
+def write_flash(esp, args):
+ # set args.compress based on default behaviour:
+ # -> if either --compress or --no-compress is set, honour that
+ # -> otherwise, set --compress unless --no-stub is set
+ if args.compress is None and not args.no_compress:
+ args.compress = not args.no_stub
+
+ if not args.force and esp.CHIP_NAME != "ESP8266" and not esp.secure_download_mode:
+ # Check if secure boot is active
+ if esp.get_secure_boot_enabled():
+ for address, _ in args.addr_filename:
+ if address < 0x8000:
+ raise FatalError(
+ "Secure Boot detected, writing to flash regions < 0x8000 "
+ "is disabled to protect the bootloader. "
+ "Use --force to override, "
+ "please use with caution, otherwise it may brick your device!"
+ )
+ # Check if chip_id and min_rev in image are valid for the target in use
+ for _, argfile in args.addr_filename:
+ try:
+ image = LoadFirmwareImage(esp.CHIP_NAME, argfile)
+ except (FatalError, struct.error, RuntimeError):
+ continue
+ finally:
+ argfile.seek(0) # LoadFirmwareImage changes the file handle position
+ if image.chip_id != esp.IMAGE_CHIP_ID:
+ raise FatalError(
+ f"{argfile.name} is not an {esp.CHIP_NAME} image. "
+ "Use --force to flash anyway."
+ )
+
+ # this logic below decides which min_rev to use, min_rev or min/max_rev_full
+ if image.max_rev_full == 0: # image does not have max/min_rev_full fields
+ use_rev_full_fields = False
+ elif image.max_rev_full == 65535: # image has default value of max_rev_full
+ use_rev_full_fields = True
+ if (
+ image.min_rev_full == 0 and image.min_rev != 0
+ ): # min_rev_full is not set, min_rev is used
+ use_rev_full_fields = False
+ else: # max_rev_full set to a version
+ use_rev_full_fields = True
+
+ if use_rev_full_fields:
+ rev = esp.get_chip_revision()
+ if rev < image.min_rev_full or rev > image.max_rev_full:
+ error_str = f"{argfile.name} requires chip revision in range "
+ error_str += (
+ f"[v{image.min_rev_full // 100}.{image.min_rev_full % 100} - "
+ )
+ if image.max_rev_full == 65535:
+ error_str += "max rev not set] "
+ else:
+ error_str += (
+ f"v{image.max_rev_full // 100}.{image.max_rev_full % 100}] "
+ )
+ error_str += f"(this chip is revision v{rev // 100}.{rev % 100})"
+ raise FatalError(f"{error_str}. Use --force to flash anyway.")
+ else:
+ # In IDF, image.min_rev is set based on Kconfig option.
+ # For C3 chip, image.min_rev is the Minor revision
+ # while for the rest chips it is the Major revision.
+ if esp.CHIP_NAME == "ESP32-C3":
+ rev = esp.get_minor_chip_version()
+ else:
+ rev = esp.get_major_chip_version()
+ if rev < image.min_rev:
+ raise FatalError(
+ f"{argfile.name} requires chip revision "
+ f"{image.min_rev} or higher (this chip is revision {rev}). "
+ "Use --force to flash anyway."
+ )
+
+ # In case we have encrypted files to write,
+ # we first do few sanity checks before actual flash
+ if args.encrypt or args.encrypt_files is not None:
+ do_write = True
+
+ if not esp.secure_download_mode:
+ if esp.get_encrypted_download_disabled():
+ raise FatalError(
+ "This chip has encrypt functionality "
+ "in UART download mode disabled. "
+ "This is the Flash Encryption configuration for Production mode "
+ "instead of Development mode."
+ )
+
+ crypt_cfg_efuse = esp.get_flash_crypt_config()
+
+ if crypt_cfg_efuse is not None and crypt_cfg_efuse != 0xF:
+ print("Unexpected FLASH_CRYPT_CONFIG value: 0x%x" % (crypt_cfg_efuse))
+ do_write = False
+
+ enc_key_valid = esp.is_flash_encryption_key_valid()
+
+ if not enc_key_valid:
+ print("Flash encryption key is not programmed")
+ do_write = False
+
+ # Determine which files list contain the ones to encrypt
+ files_to_encrypt = args.addr_filename if args.encrypt else args.encrypt_files
+
+ for address, argfile in files_to_encrypt:
+ if address % esp.FLASH_ENCRYPTED_WRITE_ALIGN:
+ print(
+ "File %s address 0x%x is not %d byte aligned, can't flash encrypted"
+ % (argfile.name, address, esp.FLASH_ENCRYPTED_WRITE_ALIGN)
+ )
+ do_write = False
+
+ if not do_write and not args.ignore_flash_encryption_efuse_setting:
+ raise FatalError(
+ "Can't perform encrypted flash write, "
+ "consult Flash Encryption documentation for more information"
+ )
+ else:
+ if not args.force and esp.CHIP_NAME != "ESP8266":
+ # ESP32 does not support `get_security_info()` and `secure_download_mode`
+ if (
+ esp.CHIP_NAME != "ESP32"
+ and esp.secure_download_mode
+ and bin(esp.get_security_info()["flash_crypt_cnt"]).count("1") & 1 != 0
+ ):
+ raise FatalError(
+ "WARNING: Detected flash encryption and "
+ "secure download mode enabled.\n"
+ "Flashing plaintext binary may brick your device! "
+ "Use --force to override the warning."
+ )
+
+ if (
+ not esp.secure_download_mode
+ and esp.get_encrypted_download_disabled()
+ and esp.get_flash_encryption_enabled()
+ ):
+ raise FatalError(
+ "WARNING: Detected flash encryption enabled and "
+ "download manual encrypt disabled.\n"
+ "Flashing plaintext binary may brick your device! "
+ "Use --force to override the warning."
+ )
+
+ set_flash_size = (
+ flash_size_bytes(args.flash_size)
+ if args.flash_size not in ["detect", "keep"]
+ else None
+ )
+ if esp.secure_download_mode:
+ flash_end = set_flash_size
+ else: # Check against real flash chip size if not in SDM
+ flash_end_str = detect_flash_size(esp)
+ flash_end = flash_size_bytes(flash_end_str)
+ if set_flash_size and set_flash_size > flash_end:
+ print(
+ f"WARNING: Set --flash_size {args.flash_size} "
+ f"is larger than the available flash size of {flash_end_str}."
+ )
+
+ # Verify file sizes fit in the set --flash_size, or real flash size if smaller
+ flash_end = min(set_flash_size, flash_end) if set_flash_size else flash_end
+ if flash_end is not None:
+ for address, argfile in args.addr_filename:
+ argfile.seek(0, os.SEEK_END)
+ if address + argfile.tell() > flash_end:
+ raise FatalError(
+ f"File {argfile.name} (length {argfile.tell()}) at offset "
+ f"{address} will not fit in {flash_end} bytes of flash. "
+ "Change the --flash_size argument, or flashing address."
+ )
+ argfile.seek(0)
+
+ if args.erase_all:
+ erase_flash(esp, args)
+ else:
+ for address, argfile in args.addr_filename:
+ argfile.seek(0, os.SEEK_END)
+ write_end = address + argfile.tell()
+ argfile.seek(0)
+ bytes_over = address % esp.FLASH_SECTOR_SIZE
+ if bytes_over != 0:
+ print(
+ "WARNING: Flash address {:#010x} is not aligned "
+ "to a {:#x} byte flash sector. "
+ "{:#x} bytes before this address will be erased.".format(
+ address, esp.FLASH_SECTOR_SIZE, bytes_over
+ )
+ )
+ # Print the address range of to-be-erased flash memory region
+ print(
+ "Flash will be erased from {:#010x} to {:#010x}...".format(
+ address - bytes_over,
+ div_roundup(write_end, esp.FLASH_SECTOR_SIZE)
+ * esp.FLASH_SECTOR_SIZE
+ - 1,
+ )
+ )
+
+ """ Create a list describing all the files we have to flash.
+ Each entry holds an "encrypt" flag marking whether the file needs encryption or not.
+ This list needs to be sorted.
+
+ First, append to each entry of our addr_filename list the flag args.encrypt
+ E.g., if addr_filename is [(0x1000, "partition.bin"), (0x8000, "bootloader")],
+ all_files will be [
+ (0x1000, "partition.bin", args.encrypt),
+ (0x8000, "bootloader", args.encrypt)
+ ],
+ where, of course, args.encrypt is either True or False
+ """
+ all_files = [
+ (offs, filename, args.encrypt) for (offs, filename) in args.addr_filename
+ ]
+
+ """
+ Now do the same with encrypt_files list, if defined.
+ In this case, the flag is True
+ """
+ if args.encrypt_files is not None:
+ encrypted_files_flag = [
+ (offs, filename, True) for (offs, filename) in args.encrypt_files
+ ]
+
+ # Concatenate both lists and sort them.
+ # As both list are already sorted, we could simply do a merge instead,
+ # but for the sake of simplicity and because the lists are very small,
+ # let's use sorted.
+ all_files = sorted(all_files + encrypted_files_flag, key=lambda x: x[0])
+
+ for address, argfile, encrypted in all_files:
+ compress = args.compress
+
+ # Check whether we can compress the current file before flashing
+ if compress and encrypted:
+ print("\nWARNING: - compress and encrypt options are mutually exclusive ")
+ print("Will flash %s uncompressed" % argfile.name)
+ compress = False
+
+ image = argfile.read()
+
+ if len(image) == 0:
+ print("WARNING: File %s is empty" % argfile.name)
+ continue
+
+ image = pad_to(image, esp.FLASH_ENCRYPTED_WRITE_ALIGN if encrypted else 4)
+
+ if args.no_stub:
+ print("Erasing flash...")
+
+ # It is not possible to write to not aligned addresses without stub,
+ # so there are added 0xFF (erase) bytes at the beginning of the image
+ # to align it.
+ bytes_over = address % esp.FLASH_SECTOR_SIZE
+ address -= bytes_over
+ image = b"\xFF" * bytes_over + image
+
+ if not esp.secure_download_mode and not esp.get_secure_boot_enabled():
+ image = _update_image_flash_params(esp, address, args, image)
+ else:
+ print(
+ "WARNING: Security features enabled, so not changing any flash settings."
+ )
+ calcmd5 = hashlib.md5(image).hexdigest()
+ uncsize = len(image)
+ if compress:
+ uncimage = image
+ image = zlib.compress(uncimage, 9)
+ original_image = image # Save the whole image in case retry is needed
+ # Try again if reconnect was successful
+ for attempt in range(1, esp.WRITE_FLASH_ATTEMPTS + 1):
+ try:
+ if compress:
+ # Decompress the compressed binary a block at a time,
+ # to dynamically calculate the timeout based on the real write size
+ decompress = zlib.decompressobj()
+ blocks = esp.flash_defl_begin(uncsize, len(image), address)
+ else:
+ blocks = esp.flash_begin(
+ uncsize, address, begin_rom_encrypted=encrypted
+ )
+ argfile.seek(0) # in case we need it again
+ seq = 0
+ bytes_sent = 0 # bytes sent on wire
+ bytes_written = 0 # bytes written to flash
+ t = time.time()
+
+ timeout = DEFAULT_TIMEOUT
+
+ while len(image) > 0:
+ print_overwrite(
+ "Writing at 0x%08x... (%d %%)"
+ % (address + bytes_written, 100 * (seq + 1) // blocks)
+ )
+ sys.stdout.flush()
+ block = image[0 : esp.FLASH_WRITE_SIZE]
+ if compress:
+ # feeding each compressed block into the decompressor lets us
+ # see block-by-block how much will be written
+ block_uncompressed = len(decompress.decompress(block))
+ bytes_written += block_uncompressed
+ block_timeout = max(
+ DEFAULT_TIMEOUT,
+ timeout_per_mb(
+ ERASE_WRITE_TIMEOUT_PER_MB, block_uncompressed
+ ),
+ )
+ if not esp.IS_STUB:
+ timeout = block_timeout # ROM code writes block to flash before ACKing
+ esp.flash_defl_block(block, seq, timeout=timeout)
+ if esp.IS_STUB:
+ # Stub ACKs when block is received,
+ # then writes to flash while receiving the block after it
+ timeout = block_timeout
+ else:
+ # Pad the last block
+ block = block + b"\xff" * (esp.FLASH_WRITE_SIZE - len(block))
+ if encrypted:
+ esp.flash_encrypt_block(block, seq)
+ else:
+ esp.flash_block(block, seq)
+ bytes_written += len(block)
+ bytes_sent += len(block)
+ image = image[esp.FLASH_WRITE_SIZE :]
+ seq += 1
+ break
+ except SerialException:
+ if attempt == esp.WRITE_FLASH_ATTEMPTS or encrypted:
+ # Already retried once or encrypted mode is disabled because of security reasons
+ raise
+ print("\nLost connection, retrying...")
+ esp._port.close()
+ print("Waiting for the chip to reconnect", end="")
+ for _ in range(DEFAULT_CONNECT_ATTEMPTS):
+ try:
+ time.sleep(1)
+ esp._port.open()
+ print() # Print new line which was suppressed by print(".")
+ esp.connect()
+ if esp.IS_STUB:
+ # Hack to bypass the stub overwrite check
+ esp.IS_STUB = False
+ # Reflash stub because chip was reset
+ esp = esp.run_stub()
+ image = original_image
+ break
+ except SerialException:
+ print(".", end="")
+ sys.stdout.flush()
+ else:
+ raise # Reconnect limit reached
+
+ if esp.IS_STUB:
+ # Stub only writes each block to flash after 'ack'ing the receive,
+ # so do a final dummy operation which will not be 'ack'ed
+ # until the last block has actually been written out to flash
+ esp.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR, timeout=timeout)
+
+ t = time.time() - t
+ speed_msg = ""
+ if compress:
+ if t > 0.0:
+ speed_msg = " (effective %.1f kbit/s)" % (uncsize / t * 8 / 1000)
+ print_overwrite(
+ "Wrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s..."
+ % (uncsize, bytes_sent, address, t, speed_msg),
+ last_line=True,
+ )
+ else:
+ if t > 0.0:
+ speed_msg = " (%.1f kbit/s)" % (bytes_written / t * 8 / 1000)
+ print_overwrite(
+ "Wrote %d bytes at 0x%08x in %.1f seconds%s..."
+ % (bytes_written, address, t, speed_msg),
+ last_line=True,
+ )
+
+ if not encrypted and not esp.secure_download_mode:
+ try:
+ res = esp.flash_md5sum(address, uncsize)
+ if res != calcmd5:
+ print("File md5: %s" % calcmd5)
+ print("Flash md5: %s" % res)
+ print(
+ "MD5 of 0xFF is %s"
+ % (hashlib.md5(b"\xff" * uncsize).hexdigest())
+ )
+ raise FatalError("MD5 of file does not match data in flash!")
+ else:
+ print("Hash of data verified.")
+ except NotImplementedInROMError:
+ pass
+
+ print("\nLeaving...")
+
+ if esp.IS_STUB:
+ # skip sending flash_finish to ROM loader here,
+ # as it causes the loader to exit and run user code
+ esp.flash_begin(0, 0)
+
+ # Get the "encrypted" flag for the last file flashed
+ # Note: all_files list contains triplets like:
+ # (address: Integer, filename: String, encrypted: Boolean)
+ last_file_encrypted = all_files[-1][2]
+
+ # Check whether the last file flashed was compressed or not
+ if args.compress and not last_file_encrypted:
+ esp.flash_defl_finish(False)
+ else:
+ esp.flash_finish(False)
+
+ if args.verify:
+ print("Verifying just-written flash...")
+ print(
+ "(This option is deprecated, "
+ "flash contents are now always read back after flashing.)"
+ )
+ # If some encrypted files have been flashed,
+ # print a warning saying that we won't check them
+ if args.encrypt or args.encrypt_files is not None:
+ print("WARNING: - cannot verify encrypted files, they will be ignored")
+ # Call verify_flash function only if there is at least
+ # one non-encrypted file flashed
+ if not args.encrypt:
+ verify_flash(esp, args)
+
+
+def image_info(args):
+ def v2():
+ def get_key_from_value(dict, val):
+ """Get key from value in dictionary"""
+ for key, value in dict.items():
+ if value == val:
+ return key
+ return None
+
+ print()
+ title = "{} image header".format(args.chip.upper())
+ print(title)
+ print("=" * len(title))
+ print("Image version: {}".format(image.version))
+ print(
+ "Entry point: {:#8x}".format(image.entrypoint)
+ if image.entrypoint != 0
+ else "Entry point not set"
+ )
+
+ print("Segments: {}".format(len(image.segments)))
+
+ # Flash size
+ flash_s_bits = image.flash_size_freq & 0xF0 # high four bits
+ flash_s = get_key_from_value(image.ROM_LOADER.FLASH_SIZES, flash_s_bits)
+ print(
+ "Flash size: {}".format(flash_s)
+ if flash_s is not None
+ else "WARNING: Invalid flash size ({:#02x})".format(flash_s_bits)
+ )
+
+ # Flash frequency
+ flash_fr_bits = image.flash_size_freq & 0x0F # low four bits
+ flash_fr = get_key_from_value(image.ROM_LOADER.FLASH_FREQUENCY, flash_fr_bits)
+ print(
+ "Flash freq: {}".format(flash_fr)
+ if flash_fr is not None
+ else "WARNING: Invalid flash frequency ({:#02x})".format(flash_fr_bits)
+ )
+
+ # Flash mode
+ flash_mode = get_key_from_value(FLASH_MODES, image.flash_mode)
+ print(
+ "Flash mode: {}".format(flash_mode.upper())
+ if flash_mode is not None
+ else "WARNING: Invalid flash mode ({})".format(image.flash_mode)
+ )
+
+ # Extended header (ESP32 and later only)
+ if args.chip != "esp8266":
+ print()
+ title = "{} extended image header".format(args.chip.upper())
+ print(title)
+ print("=" * len(title))
+ print(
+ f"WP pin: {image.wp_pin:#02x}",
+ *["(disabled)"] if image.wp_pin == image.WP_PIN_DISABLED else [],
+ )
+ print(
+ "Flash pins drive settings: "
+ "clk_drv: {:#02x}, q_drv: {:#02x}, d_drv: {:#02x}, "
+ "cs0_drv: {:#02x}, hd_drv: {:#02x}, wp_drv: {:#02x}".format(
+ image.clk_drv,
+ image.q_drv,
+ image.d_drv,
+ image.cs_drv,
+ image.hd_drv,
+ image.wp_drv,
+ )
+ )
+ try:
+ chip = next(
+ chip
+ for chip in CHIP_DEFS.values()
+ if getattr(chip, "IMAGE_CHIP_ID", None) == image.chip_id
+ )
+ print(f"Chip ID: {image.chip_id} ({chip.CHIP_NAME})")
+ except StopIteration:
+ print(f"Chip ID: {image.chip_id} (Unknown ID)")
+ print(
+ "Minimal chip revision: "
+ f"v{image.min_rev_full // 100}.{image.min_rev_full % 100}, "
+ f"(legacy min_rev = {image.min_rev})"
+ )
+ print(
+ "Maximal chip revision: "
+ f"v{image.max_rev_full // 100}.{image.max_rev_full % 100}"
+ )
+ print()
+
+ # Segments overview
+ title = "Segments information"
+ print(title)
+ print("=" * len(title))
+ headers_str = "{:>7} {:>7} {:>10} {:>10} {:10}"
+ print(
+ headers_str.format(
+ "Segment", "Length", "Load addr", "File offs", "Memory types"
+ )
+ )
+ print(
+ "{} {} {} {} {}".format("-" * 7, "-" * 7, "-" * 10, "-" * 10, "-" * 12)
+ )
+ format_str = "{:7} {:#07x} {:#010x} {:#010x} {}"
+ app_desc = None
+ bootloader_desc = None
+ for idx, seg in enumerate(image.segments):
+ segs = seg.get_memory_type(image)
+ seg_name = ", ".join(segs)
+ if "DROM" in segs: # The DROM segment starts with the esp_app_desc_t struct
+ app_desc = seg.data[:256]
+ elif "DRAM" in segs:
+ # The DRAM segment starts with the esp_bootloader_desc_t struct
+ if len(seg.data) >= 80:
+ bootloader_desc = seg.data[:80]
+ print(
+ format_str.format(idx, len(seg.data), seg.addr, seg.file_offs, seg_name)
+ )
+ print()
+
+ # Footer
+ title = f"{args.chip.upper()} image footer"
+ print(title)
+ print("=" * len(title))
+ calc_checksum = image.calculate_checksum()
+ print(
+ "Checksum: {:#02x} ({})".format(
+ image.checksum,
+ (
+ "valid"
+ if image.checksum == calc_checksum
+ else "invalid - calculated {:02x}".format(calc_checksum)
+ ),
+ )
+ )
+ try:
+ digest_msg = "Not appended"
+ if image.append_digest:
+ is_valid = image.stored_digest == image.calc_digest
+ digest_msg = "{} ({})".format(
+ hexify(image.calc_digest, uppercase=False),
+ "valid" if is_valid else "invalid",
+ )
+ print("Validation hash: {}".format(digest_msg))
+ except AttributeError:
+ pass # ESP8266 image has no append_digest field
+
+ if app_desc:
+ APP_DESC_STRUCT_FMT = " 1 else ""))
+
+ image.verify()
+
+ if args.output is None:
+ args.output = image.default_output_name(args.input)
+ image.save(args.output)
+
+ print("Successfully created {} image.".format(args.chip))
+
+
+def read_mac(esp, args):
+ def print_mac(label, mac):
+ print("%s: %s" % (label, ":".join(map(lambda x: "%02x" % x, mac))))
+
+ eui64 = esp.read_mac("EUI64")
+ if eui64:
+ print_mac("MAC", eui64)
+ print_mac("BASE MAC", esp.read_mac("BASE_MAC"))
+ print_mac("MAC_EXT", esp.read_mac("MAC_EXT"))
+ else:
+ print_mac("MAC", esp.read_mac("BASE_MAC"))
+
+
+def chip_id(esp, args):
+ try:
+ chipid = esp.chip_id()
+ print("Chip ID: 0x%08x" % chipid)
+ except NotSupportedError:
+ print("Warning: %s has no Chip ID. Reading MAC instead." % esp.CHIP_NAME)
+ read_mac(esp, args)
+
+
+def erase_flash(esp, args):
+ if not args.force and esp.CHIP_NAME != "ESP8266" and not esp.secure_download_mode:
+ if esp.get_flash_encryption_enabled() or esp.get_secure_boot_enabled():
+ raise FatalError(
+ "Active security features detected, "
+ "erasing flash is disabled as a safety measure. "
+ "Use --force to override, "
+ "please use with caution, otherwise it may brick your device!"
+ )
+ print("Erasing flash (this may take a while)...")
+ t = time.time()
+ esp.erase_flash()
+ print("Chip erase completed successfully in %.1fs" % (time.time() - t))
+
+
+def erase_region(esp, args):
+ if not args.force and esp.CHIP_NAME != "ESP8266" and not esp.secure_download_mode:
+ if esp.get_flash_encryption_enabled() or esp.get_secure_boot_enabled():
+ raise FatalError(
+ "Active security features detected, "
+ "erasing flash is disabled as a safety measure. "
+ "Use --force to override, "
+ "please use with caution, otherwise it may brick your device!"
+ )
+ print("Erasing region (may be slow depending on size)...")
+ t = time.time()
+ esp.erase_region(args.address, args.size)
+ print("Erase completed successfully in %.1f seconds." % (time.time() - t))
+
+
+def run(esp, args):
+ esp.run()
+
+
+def detect_flash_id(esp):
+ flash_id = esp.flash_id()
+ print("Manufacturer: %02x" % (flash_id & 0xFF))
+ flid_lowbyte = (flash_id >> 16) & 0xFF
+ print("Device: %02x%02x" % ((flash_id >> 8) & 0xFF, flid_lowbyte))
+ print(
+ "Detected flash size: %s" % (DETECTED_FLASH_SIZES.get(flid_lowbyte, "Unknown"))
+ )
+
+
+def flash_id(esp, args):
+ detect_flash_id(esp)
+ flash_type = esp.flash_type()
+ flash_type_dict = {0: "quad (4 data lines)", 1: "octal (8 data lines)"}
+ flash_type_str = flash_type_dict.get(flash_type)
+ if flash_type_str:
+ print(f"Flash type set in eFuse: {flash_type_str}")
+ esp.get_flash_voltage()
+
+
+def read_flash_sfdp(esp, args):
+ detect_flash_id(esp)
+
+ sfdp = esp.read_spiflash_sfdp(args.addr, args.bytes * 8)
+ print(f"SFDP[{args.addr}..{args.addr+args.bytes-1}]: ", end="")
+ for i in range(args.bytes):
+ print(f"{sfdp&0xff:02X} ", end="")
+ sfdp = sfdp >> 8
+ print()
+
+
+def read_flash(esp, args):
+ if args.no_progress:
+ flash_progress = None
+ else:
+
+ def flash_progress(progress, length):
+ msg = "%d (%d %%)" % (progress, progress * 100.0 / length)
+ padding = "\b" * len(msg)
+ if progress == length:
+ padding = "\n"
+ sys.stdout.write(msg + padding)
+ sys.stdout.flush()
+
+ t = time.time()
+ data = esp.read_flash(args.address, args.size, flash_progress)
+ t = time.time() - t
+ speed_msg = " ({:.1f} kbit/s)".format(len(data) / t * 8 / 1000) if t > 0.0 else ""
+ print_overwrite(
+ "Read {:d} bytes at {:#010x} in {:.1f} seconds{}...".format(
+ len(data), args.address, t, speed_msg
+ ),
+ last_line=True,
+ )
+ with open(args.filename, "wb") as f:
+ f.write(data)
+
+
+def verify_flash(esp, args):
+ differences = False
+
+ for address, argfile in args.addr_filename:
+ image = pad_to(argfile.read(), 4)
+ argfile.seek(0) # rewind in case we need it again
+
+ image = _update_image_flash_params(esp, address, args, image)
+
+ image_size = len(image)
+ print(
+ "Verifying 0x%x (%d) bytes @ 0x%08x in flash against %s..."
+ % (image_size, image_size, address, argfile.name)
+ )
+ # Try digest first, only read if there are differences.
+ digest = esp.flash_md5sum(address, image_size)
+ expected_digest = hashlib.md5(image).hexdigest()
+ if digest == expected_digest:
+ print("-- verify OK (digest matched)")
+ continue
+ else:
+ differences = True
+ if getattr(args, "diff", "no") != "yes":
+ print("-- verify FAILED (digest mismatch)")
+ continue
+
+ flash = esp.read_flash(address, image_size)
+ assert flash != image
+ diff = [i for i in range(image_size) if flash[i] != image[i]]
+ print(
+ "-- verify FAILED: %d differences, first @ 0x%08x"
+ % (len(diff), address + diff[0])
+ )
+ for d in diff:
+ flash_byte = flash[d]
+ image_byte = image[d]
+ print(" %08x %02x %02x" % (address + d, flash_byte, image_byte))
+ if differences:
+ raise FatalError("Verify failed.")
+
+
+def read_flash_status(esp, args):
+ print("Status value: 0x%04x" % esp.read_status(args.bytes))
+
+
+def write_flash_status(esp, args):
+ fmt = "0x%%0%dx" % (args.bytes * 2)
+ args.value = args.value & ((1 << (args.bytes * 8)) - 1)
+ print(("Initial flash status: " + fmt) % esp.read_status(args.bytes))
+ print(("Setting flash status: " + fmt) % args.value)
+ esp.write_status(args.value, args.bytes, args.non_volatile)
+ print(("After flash status: " + fmt) % esp.read_status(args.bytes))
+
+
+# The following mapping was taken from the ROM code
+# This mapping is same across all targets in the ROM
+SECURITY_INFO_FLAG_MAP = {
+ "SECURE_BOOT_EN": (1 << 0),
+ "SECURE_BOOT_AGGRESSIVE_REVOKE": (1 << 1),
+ "SECURE_DOWNLOAD_ENABLE": (1 << 2),
+ "SECURE_BOOT_KEY_REVOKE0": (1 << 3),
+ "SECURE_BOOT_KEY_REVOKE1": (1 << 4),
+ "SECURE_BOOT_KEY_REVOKE2": (1 << 5),
+ "SOFT_DIS_JTAG": (1 << 6),
+ "HARD_DIS_JTAG": (1 << 7),
+ "DIS_USB": (1 << 8),
+ "DIS_DOWNLOAD_DCACHE": (1 << 9),
+ "DIS_DOWNLOAD_ICACHE": (1 << 10),
+}
+
+
+# Get the status of respective security flag
+def get_security_flag_status(flag_name, flags_value):
+ try:
+ return (flags_value & SECURITY_INFO_FLAG_MAP[flag_name]) != 0
+ except KeyError:
+ raise ValueError(f"Invalid flag name: {flag_name}")
+
+
+def get_security_info(esp, args):
+ si = esp.get_security_info()
+ print()
+ title = "Security Information:"
+ print(title)
+ print("=" * len(title))
+ print("Flags: {:#010x} ({})".format(si["flags"], bin(si["flags"])))
+ if esp.KEY_PURPOSES:
+ print(f"Key Purposes: {si['key_purposes']}")
+ desc = "\n ".join(
+ [
+ f"BLOCK_KEY{key_num} - {esp.KEY_PURPOSES.get(purpose, 'UNKNOWN')}"
+ for key_num, purpose in enumerate(si["key_purposes"])
+ if key_num <= esp.EFUSE_MAX_KEY
+ ]
+ )
+ print(f" {desc}")
+ if si["chip_id"] is not None and si["api_version"] is not None:
+ print("Chip ID: {}".format(si["chip_id"]))
+ print("API Version: {}".format(si["api_version"]))
+
+ flags = si["flags"]
+
+ if get_security_flag_status("SECURE_BOOT_EN", flags):
+ print("Secure Boot: Enabled")
+ if get_security_flag_status("SECURE_BOOT_AGGRESSIVE_REVOKE", flags):
+ print("Secure Boot Aggressive key revocation: Enabled")
+
+ revoked_keys = []
+ for i, key in enumerate(
+ [
+ "SECURE_BOOT_KEY_REVOKE0",
+ "SECURE_BOOT_KEY_REVOKE1",
+ "SECURE_BOOT_KEY_REVOKE2",
+ ]
+ ):
+ if get_security_flag_status(key, flags):
+ revoked_keys.append(i)
+
+ if len(revoked_keys) > 0:
+ print("Secure Boot Key Revocation Status:\n")
+ for i in revoked_keys:
+ print(f"\tSecure Boot Key{i} is Revoked\n")
+
+ else:
+ print("Secure Boot: Disabled")
+
+ flash_crypt_cnt = bin(si["flash_crypt_cnt"])
+ if (flash_crypt_cnt.count("1") % 2) != 0:
+ print("Flash Encryption: Enabled")
+ else:
+ print("Flash Encryption: Disabled")
+
+ CRYPT_CNT_STRING = "SPI Boot Crypt Count (SPI_BOOT_CRYPT_CNT)"
+ if esp.CHIP_NAME == "esp32":
+ CRYPT_CNT_STRING = "Flash Crypt Count (FLASH_CRYPT_CNT)"
+
+ print(f"{CRYPT_CNT_STRING}: {si['flash_crypt_cnt']:#x}")
+
+ if get_security_flag_status("DIS_DOWNLOAD_DCACHE", flags):
+ print("Dcache in UART download mode: Disabled")
+
+ if get_security_flag_status("DIS_DOWNLOAD_ICACHE", flags):
+ print("Icache in UART download mode: Disabled")
+
+ hard_dis_jtag = get_security_flag_status("HARD_DIS_JTAG", flags)
+ soft_dis_jtag = get_security_flag_status("SOFT_DIS_JTAG", flags)
+ if hard_dis_jtag:
+ print("JTAG: Permanently Disabled")
+ elif soft_dis_jtag:
+ print("JTAG: Software Access Disabled")
+ if get_security_flag_status("DIS_USB", flags):
+ print("USB Access: Disabled")
+
+
+def merge_bin(args):
+ try:
+ chip_class = CHIP_DEFS[args.chip]
+ except KeyError:
+ msg = (
+ "Please specify the chip argument"
+ if args.chip == "auto"
+ else f"Invalid chip choice: '{args.chip}'"
+ )
+ msg = f"{msg} (choose from {', '.join(CHIP_LIST)})"
+ raise FatalError(msg)
+
+ # sort the files by offset.
+ # The AddrFilenamePairAction has already checked for overlap
+ input_files = sorted(args.addr_filename, key=lambda x: x[0])
+ if not input_files:
+ raise FatalError("No input files specified")
+ first_addr = input_files[0][0]
+ if first_addr < args.target_offset:
+ raise FatalError(
+ f"Output file target offset is {args.target_offset:#x}. "
+ f"Input file offset {first_addr:#x} is before this."
+ )
+
+ if args.format == "uf2":
+ with UF2Writer(
+ chip_class.UF2_FAMILY_ID,
+ args.output,
+ args.chunk_size,
+ md5_enabled=not args.md5_disable,
+ ) as writer:
+ for addr, argfile in input_files:
+ print(f"Adding {argfile.name} at {addr:#x}")
+ image = argfile.read()
+ image = _update_image_flash_params(chip_class, addr, args, image)
+ writer.add_file(addr, image)
+ print(
+ f"Wrote {os.path.getsize(args.output):#x} bytes to file {args.output}, "
+ f"ready to be flashed with any ESP USB Bridge"
+ )
+
+ elif args.format == "raw":
+ with open(args.output, "wb") as of:
+
+ def pad_to(flash_offs):
+ # account for output file offset if there is any
+ of.write(b"\xff" * (flash_offs - args.target_offset - of.tell()))
+
+ for addr, argfile in input_files:
+ pad_to(addr)
+ image = argfile.read()
+ image = _update_image_flash_params(chip_class, addr, args, image)
+ of.write(image)
+ if args.fill_flash_size:
+ pad_to(flash_size_bytes(args.fill_flash_size))
+ print(
+ f"Wrote {of.tell():#x} bytes to file {args.output}, "
+ f"ready to flash to offset {args.target_offset:#x}"
+ )
+ elif args.format == "hex":
+ out = IntelHex()
+ if len(input_files) == 1:
+ print(
+ "WARNING: Only one input file specified, output may include "
+ "additional padding if input file was previously merged. "
+ "Please refer to the documentation for more information: "
+ "https://docs.espressif.com/projects/esptool/en/latest/esptool/basic-commands.html#hex-output-format" # noqa E501
+ )
+ for addr, argfile in input_files:
+ ihex = IntelHex()
+ image = argfile.read()
+ image = _update_image_flash_params(chip_class, addr, args, image)
+ ihex.frombytes(image, addr)
+ out.merge(ihex)
+ out.write_hex_file(args.output)
+ print(
+ f"Wrote {os.path.getsize(args.output):#x} bytes to file {args.output}, "
+ f"ready to flash to offset {args.target_offset:#x}"
+ )
+
+
+def version(args):
+ from . import __version__
+
+ print(__version__)
diff --git a/mixly/tools/python/esptool/config.py b/mixly/tools/python/esptool/config.py
new file mode 100644
index 00000000..ebbe3b8e
--- /dev/null
+++ b/mixly/tools/python/esptool/config.py
@@ -0,0 +1,93 @@
+# SPDX-FileCopyrightText: 2014-2023 Espressif Systems (Shanghai) CO LTD,
+# other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import configparser
+import os
+
+CONFIG_OPTIONS = [
+ "timeout",
+ "chip_erase_timeout",
+ "max_timeout",
+ "sync_timeout",
+ "md5_timeout_per_mb",
+ "erase_region_timeout_per_mb",
+ "erase_write_timeout_per_mb",
+ "mem_end_rom_timeout",
+ "serial_write_timeout",
+ "connect_attempts",
+ "write_block_attempts",
+ "reset_delay",
+ "open_port_attempts",
+ "custom_reset_sequence",
+]
+
+
+def _validate_config_file(file_path, verbose=False):
+ if not os.path.exists(file_path):
+ return False
+
+ cfg = configparser.RawConfigParser()
+ try:
+ cfg.read(file_path, encoding="UTF-8")
+ # Only consider it a valid config file if it contains [esptool] section
+ if cfg.has_section("esptool"):
+ if verbose:
+ unknown_opts = list(set(cfg.options("esptool")) - set(CONFIG_OPTIONS))
+ unknown_opts.sort()
+ no_of_unknown_opts = len(unknown_opts)
+ if no_of_unknown_opts > 0:
+ suffix = "s" if no_of_unknown_opts > 1 else ""
+ print(
+ "Ignoring unknown config file option{}: {}".format(
+ suffix, ", ".join(unknown_opts)
+ )
+ )
+ return True
+ except (UnicodeDecodeError, configparser.Error) as e:
+ if verbose:
+ print(f"Ignoring invalid config file {file_path}: {e}")
+ return False
+
+
+def _find_config_file(dir_path, verbose=False):
+ for candidate in ("esptool.cfg", "setup.cfg", "tox.ini"):
+ cfg_path = os.path.join(dir_path, candidate)
+ if _validate_config_file(cfg_path, verbose):
+ return cfg_path
+ return None
+
+
+def load_config_file(verbose=False):
+ set_with_env_var = False
+ env_var_path = os.environ.get("ESPTOOL_CFGFILE")
+ if env_var_path is not None and _validate_config_file(env_var_path):
+ cfg_file_path = env_var_path
+ set_with_env_var = True
+ else:
+ home_dir = os.path.expanduser("~")
+ os_config_dir = (
+ f"{home_dir}/.config/esptool"
+ if os.name == "posix"
+ else f"{home_dir}/AppData/Local/esptool/"
+ )
+ # Search priority: 1) current dir, 2) OS specific config dir, 3) home dir
+ for dir_path in (os.getcwd(), os_config_dir, home_dir):
+ cfg_file_path = _find_config_file(dir_path, verbose)
+ if cfg_file_path:
+ break
+
+ cfg = configparser.ConfigParser()
+ cfg["esptool"] = {} # Create an empty esptool config for when no file is found
+
+ if cfg_file_path is not None:
+ # If config file is found and validated, read and parse it
+ cfg.read(cfg_file_path)
+ if verbose:
+ msg = " (set with ESPTOOL_CFGFILE)" if set_with_env_var else ""
+ print(
+ f"Loaded custom configuration from "
+ f"{os.path.abspath(cfg_file_path)}{msg}"
+ )
+ return cfg, cfg_file_path
diff --git a/mixly/tools/python/esptool/loader.py b/mixly/tools/python/esptool/loader.py
new file mode 100644
index 00000000..ca3ab932
--- /dev/null
+++ b/mixly/tools/python/esptool/loader.py
@@ -0,0 +1,1719 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import base64
+import hashlib
+import itertools
+import json
+import os
+import re
+import string
+import struct
+import sys
+import time
+from typing import Optional
+
+
+from .config import load_config_file
+from .reset import (
+ ClassicReset,
+ CustomReset,
+ DEFAULT_RESET_DELAY,
+ HardReset,
+ USBJTAGSerialReset,
+ UnixTightReset,
+)
+from .util import FatalError, NotImplementedInROMError, UnsupportedCommandError
+from .util import byte, hexify, mask_to_shift, pad_to, strip_chip_name
+
+try:
+ import serial
+except ImportError:
+ print(
+ "Pyserial is not installed for %s. "
+ "Check the README for installation instructions." % (sys.executable)
+ )
+ raise
+
+# check 'serial' is 'pyserial' and not 'serial'
+# ref. https://github.com/espressif/esptool/issues/269
+try:
+ if "serialization" in serial.__doc__ and "deserialization" in serial.__doc__:
+ raise ImportError(
+ "esptool.py depends on pyserial, but there is a conflict with a currently "
+ "installed package named 'serial'.\n"
+ "You may work around this by 'pip uninstall serial; pip install pyserial' "
+ "but this may break other installed Python software "
+ "that depends on 'serial'.\n"
+ "There is no good fix for this right now, "
+ "apart from configuring virtualenvs. "
+ "See https://github.com/espressif/esptool/issues/269#issuecomment-385298196"
+ " for discussion of the underlying issue(s)."
+ )
+except TypeError:
+ pass # __doc__ returns None for pyserial
+
+try:
+ import serial.tools.list_ports as list_ports
+except ImportError:
+ print(
+ "The installed version (%s) of pyserial appears to be too old for esptool.py "
+ "(Python interpreter %s). Check the README for installation instructions."
+ % (serial.VERSION, sys.executable)
+ )
+ raise
+except Exception:
+ if sys.platform == "darwin":
+ # swallow the exception, this is a known issue in pyserial+macOS Big Sur preview
+ # ref https://github.com/espressif/esptool/issues/540
+ list_ports = None
+ else:
+ raise
+
+
+cfg, _ = load_config_file()
+cfg = cfg["esptool"]
+
+# Timeout for most flash operations
+DEFAULT_TIMEOUT = cfg.getfloat("timeout", 3)
+# Timeout for full chip erase
+CHIP_ERASE_TIMEOUT = cfg.getfloat("chip_erase_timeout", 120)
+# Longest any command can run
+MAX_TIMEOUT = cfg.getfloat("max_timeout", CHIP_ERASE_TIMEOUT * 2)
+# Timeout for syncing with bootloader
+SYNC_TIMEOUT = cfg.getfloat("sync_timeout", 0.1)
+# Timeout (per megabyte) for calculating md5sum
+MD5_TIMEOUT_PER_MB = cfg.getfloat("md5_timeout_per_mb", 8)
+# Timeout (per megabyte) for erasing a region
+ERASE_REGION_TIMEOUT_PER_MB = cfg.getfloat("erase_region_timeout_per_mb", 30)
+# Timeout (per megabyte) for erasing and writing data
+ERASE_WRITE_TIMEOUT_PER_MB = cfg.getfloat("erase_write_timeout_per_mb", 40)
+# Short timeout for ESP_MEM_END, as it may never respond
+MEM_END_ROM_TIMEOUT = cfg.getfloat("mem_end_rom_timeout", 0.2)
+# Timeout for serial port write
+DEFAULT_SERIAL_WRITE_TIMEOUT = cfg.getfloat("serial_write_timeout", 10)
+# Default number of times to try connection
+DEFAULT_CONNECT_ATTEMPTS = cfg.getint("connect_attempts", 7)
+# Number of times to try writing a data block
+WRITE_BLOCK_ATTEMPTS = cfg.getint("write_block_attempts", 3)
+# Number of times to try opening the serial port
+DEFAULT_OPEN_PORT_ATTEMPTS = cfg.getint("open_port_attempts", 1)
+
+
+def timeout_per_mb(seconds_per_mb, size_bytes):
+ """Scales timeouts which are size-specific"""
+ result = seconds_per_mb * (size_bytes / 1e6)
+ if result < DEFAULT_TIMEOUT:
+ return DEFAULT_TIMEOUT
+ return result
+
+
+def check_supported_function(func, check_func):
+ """
+ Decorator implementation that wraps a check around an ESPLoader
+ bootloader function to check if it's supported.
+
+ This is used to capture the multidimensional differences in
+ functionality between the ESP8266 & ESP32 (and later chips) ROM loaders, and the
+ software stub that runs on these. Not possible to do this cleanly
+ via inheritance alone.
+ """
+
+ def inner(*args, **kwargs):
+ obj = args[0]
+ if check_func(obj):
+ return func(*args, **kwargs)
+ else:
+ raise NotImplementedInROMError(obj, func)
+
+ return inner
+
+
+def stub_function_only(func):
+ """Attribute for a function only supported in the software stub loader"""
+ return check_supported_function(func, lambda o: o.IS_STUB)
+
+
+def stub_and_esp32_function_only(func):
+ """Attribute for a function only supported by stubs or ESP32 and later chips ROM"""
+ return check_supported_function(
+ func, lambda o: o.IS_STUB or o.CHIP_NAME not in ["ESP8266"]
+ )
+
+
+def esp32s3_or_newer_function_only(func):
+ """Attribute for a function only supported by ESP32S3 and later chips ROM"""
+ return check_supported_function(
+ func, lambda o: o.CHIP_NAME not in ["ESP8266", "ESP32", "ESP32-S2"]
+ )
+
+
+class StubFlasher:
+ STUB_DIR = os.path.join(os.path.dirname(__file__), "targets", "stub_flasher")
+ # directories will be searched in the order of STUB_SUBDIRS
+ STUB_SUBDIRS = ["1", "2"]
+
+ def __init__(self, chip_name):
+ with open(self.get_json_path(chip_name)) as json_file:
+ stub = json.load(json_file)
+
+ self.text = base64.b64decode(stub["text"])
+ self.text_start = stub["text_start"]
+ self.entry = stub["entry"]
+
+ try:
+ self.data = base64.b64decode(stub["data"])
+ self.data_start = stub["data_start"]
+ except KeyError:
+ self.data = None
+ self.data_start = None
+
+ self.bss_start = stub.get("bss_start")
+
+ def get_json_path(self, chip_name):
+ chip_name = strip_chip_name(chip_name)
+ for i, subdir in enumerate(self.STUB_SUBDIRS):
+ json_path = os.path.join(self.STUB_DIR, subdir, f"{chip_name}.json")
+ if os.path.exists(json_path):
+ if i:
+ print(
+ f"Warning: Stub version {self.STUB_SUBDIRS[0]} doesn't exist, using {subdir} instead"
+ )
+
+ return json_path
+ else:
+ raise FileNotFoundError(f"Stub flasher JSON file for {chip_name} not found")
+
+ @classmethod
+ def set_preferred_stub_subdir(cls, subdir):
+ if subdir in cls.STUB_SUBDIRS:
+ cls.STUB_SUBDIRS.remove(subdir)
+ cls.STUB_SUBDIRS.insert(0, subdir)
+
+
+class ESPLoader(object):
+ """Base class providing access to ESP ROM & software stub bootloaders.
+ Subclasses provide ESP8266 & ESP32 Family specific functionality.
+
+ Don't instantiate this base class directly, either instantiate a subclass or
+ call cmds.detect_chip() which will interrogate the chip and return the
+ appropriate subclass instance. You can also use a context manager as
+ "with detect_chip() as esp:" to ensure the serial port is closed when done.
+
+ """
+
+ CHIP_NAME = "Espressif device"
+ IS_STUB = False
+ STUB_CLASS: Optional[object] = None
+ BOOTLOADER_IMAGE: Optional[object] = None
+
+ DEFAULT_PORT = "/dev/ttyUSB0"
+
+ USES_RFC2217 = False
+
+ # Commands supported by ESP8266 ROM bootloader
+ ESP_FLASH_BEGIN = 0x02
+ ESP_FLASH_DATA = 0x03
+ ESP_FLASH_END = 0x04
+ ESP_MEM_BEGIN = 0x05
+ ESP_MEM_END = 0x06
+ ESP_MEM_DATA = 0x07
+ ESP_SYNC = 0x08
+ ESP_WRITE_REG = 0x09
+ ESP_READ_REG = 0x0A
+
+ # Some commands supported by ESP32 and later chips ROM bootloader (or -8266 w/ stub)
+ ESP_SPI_SET_PARAMS = 0x0B
+ ESP_SPI_ATTACH = 0x0D
+ ESP_READ_FLASH_SLOW = 0x0E # ROM only, much slower than the stub flash read
+ ESP_CHANGE_BAUDRATE = 0x0F
+ ESP_FLASH_DEFL_BEGIN = 0x10
+ ESP_FLASH_DEFL_DATA = 0x11
+ ESP_FLASH_DEFL_END = 0x12
+ ESP_SPI_FLASH_MD5 = 0x13
+
+ # Commands supported by ESP32-S2 and later chips ROM bootloader only
+ ESP_GET_SECURITY_INFO = 0x14
+
+ # Some commands supported by stub only
+ ESP_ERASE_FLASH = 0xD0
+ ESP_ERASE_REGION = 0xD1
+ ESP_READ_FLASH = 0xD2
+ ESP_RUN_USER_CODE = 0xD3
+
+ # Flash encryption encrypted data command
+ ESP_FLASH_ENCRYPT_DATA = 0xD4
+
+ # Response code(s) sent by ROM
+ ROM_INVALID_RECV_MSG = 0x05 # response if an invalid message is received
+
+ # Maximum block sized for RAM and Flash writes, respectively.
+ ESP_RAM_BLOCK = 0x1800
+
+ FLASH_WRITE_SIZE = 0x400
+
+ # Default baudrate. The ROM auto-bauds, so we can use more or less whatever we want.
+ ESP_ROM_BAUD = 115200
+
+ # First byte of the application image
+ ESP_IMAGE_MAGIC = 0xE9
+
+ # Initial state for the checksum routine
+ ESP_CHECKSUM_MAGIC = 0xEF
+
+ # Flash sector size, minimum unit of erase.
+ FLASH_SECTOR_SIZE = 0x1000
+
+ UART_DATE_REG_ADDR = 0x60000078
+
+ # Whether the SPI peripheral sends from MSB of 32-bit register, or the MSB of valid LSB bits.
+ SPI_ADDR_REG_MSB = True
+
+ # This ROM address has a different value on each chip model
+ CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000
+
+ UART_CLKDIV_MASK = 0xFFFFF
+
+ # Memory addresses
+ IROM_MAP_START = 0x40200000
+ IROM_MAP_END = 0x40300000
+
+ # The number of bytes in the UART response that signify command status
+ STATUS_BYTES_LENGTH = 2
+
+ # Bootloader flashing offset
+ BOOTLOADER_FLASH_OFFSET = 0x0
+
+ # ROM supports an encrypted flashing mode
+ SUPPORTS_ENCRYPTED_FLASH = False
+
+ # Response to ESP_SYNC might indicate that flasher stub is running
+ # instead of the ROM bootloader
+ sync_stub_detected = False
+
+ # Device PIDs
+ USB_JTAG_SERIAL_PID = 0x1001
+
+ # Chip IDs that are no longer supported by esptool
+ UNSUPPORTED_CHIPS = {6: "ESP32-S3(beta 3)"}
+
+ # Number of attempts to write flash data
+ WRITE_FLASH_ATTEMPTS = 2
+
+ def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False):
+ """Base constructor for ESPLoader bootloader interaction
+
+ Don't call this constructor, either instantiate a specific
+ ROM class directly, or use cmds.detect_chip(). You can use the with
+ statement to ensure the serial port is closed when done.
+
+ This base class has all of the instance methods for bootloader
+ functionality supported across various chips & stub
+ loaders. Subclasses replace the functions they don't support
+ with ones which throw NotImplementedInROMError().
+
+ """
+ # True if esptool detects the ROM is in Secure Download Mode
+ self.secure_download_mode = False
+ # True if esptool detects conditions which require the stub to be disabled
+ self.stub_is_disabled = False
+
+ # Device-and-runtime-specific cache
+ self.cache = {
+ "flash_id": None,
+ "chip_id": None,
+ "uart_no": None,
+ "usb_pid": None,
+ }
+
+ if isinstance(port, str):
+ try:
+ self._port = serial.serial_for_url(
+ port, exclusive=True, do_not_open=True
+ )
+ if sys.platform == "win32":
+ # When opening a port on Windows,
+ # the RTS/DTR (active low) lines
+ # need to be set to False (pulled high)
+ # to avoid unwanted chip reset
+ self._port.rts = False
+ self._port.dtr = False
+ self._port.open()
+ except serial.serialutil.SerialException as e:
+ port_issues = [
+ [ # does not exist error
+ re.compile(r"Errno 2|FileNotFoundError", re.IGNORECASE),
+ "Check if the port is correct and ESP connected",
+ ],
+ [ # busy port error
+ re.compile(r"Access is denied", re.IGNORECASE),
+ "Check if the port is not used by another task",
+ ],
+ ]
+ if sys.platform.startswith("linux"):
+ port_issues.append(
+ [ # permission denied error
+ re.compile(r"Permission denied", re.IGNORECASE),
+ ("Try to add user into dialout or uucp group."),
+ ],
+ )
+
+ hint_msg = ""
+ for port_issue in port_issues:
+ if port_issue[0].search(str(e)):
+ hint_msg = f"\nHint: {port_issue[1]}\n"
+ break
+
+ raise FatalError(
+ f"Could not open {port}, the port is busy or doesn't exist."
+ f"\n({e})\n"
+ f"{hint_msg}"
+ )
+ else:
+ self._port = port
+ self._slip_reader = slip_reader(self._port, self.trace)
+ # setting baud rate in a separate step is a workaround for
+ # CH341 driver on some Linux versions (this opens at 9600 then
+ # sets), shouldn't matter for other platforms/drivers. See
+ # https://github.com/espressif/esptool/issues/44#issuecomment-107094446
+ self._set_port_baudrate(baud)
+ self._trace_enabled = trace_enabled
+ # set write timeout, to prevent esptool blocked at write forever.
+ try:
+ self._port.write_timeout = DEFAULT_SERIAL_WRITE_TIMEOUT
+ except NotImplementedError:
+ # no write timeout for RFC2217 ports
+ # need to set the property back to None or it will continue to fail
+ self._port.write_timeout = None
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self._port.close()
+
+ @property
+ def serial_port(self):
+ return self._port.port
+
+ def _set_port_baudrate(self, baud):
+ try:
+ self._port.baudrate = baud
+ except IOError:
+ raise FatalError(
+ "Failed to set baud rate %d. The driver may not support this rate."
+ % baud
+ )
+
+ def read(self):
+ """Read a SLIP packet from the serial port"""
+ return next(self._slip_reader)
+
+ def write(self, packet):
+ """Write bytes to the serial port while performing SLIP escaping"""
+ buf = (
+ b"\xc0"
+ + (packet.replace(b"\xdb", b"\xdb\xdd").replace(b"\xc0", b"\xdb\xdc"))
+ + b"\xc0"
+ )
+ self.trace("Write %d bytes: %s", len(buf), HexFormatter(buf))
+ self._port.write(buf)
+
+ def trace(self, message, *format_args):
+ if self._trace_enabled:
+ now = time.time()
+ try:
+ delta = now - self._last_trace
+ except AttributeError:
+ delta = 0.0
+ self._last_trace = now
+ prefix = "TRACE +%.3f " % delta
+ print(prefix + (message % format_args))
+
+ @staticmethod
+ def checksum(data, state=ESP_CHECKSUM_MAGIC):
+ """Calculate checksum of a blob, as it is defined by the ROM"""
+ for b in data:
+ state ^= b
+
+ return state
+
+ def command(
+ self,
+ op=None,
+ data=b"",
+ chk=0,
+ wait_response=True,
+ timeout=DEFAULT_TIMEOUT,
+ ):
+ """Send a request and read the response"""
+ saved_timeout = self._port.timeout
+ new_timeout = min(timeout, MAX_TIMEOUT)
+ if new_timeout != saved_timeout:
+ self._port.timeout = new_timeout
+
+ try:
+ if op is not None:
+ self.trace(
+ "command op=0x%02x data len=%s wait_response=%d "
+ "timeout=%.3f data=%s",
+ op,
+ len(data),
+ 1 if wait_response else 0,
+ timeout,
+ HexFormatter(data),
+ )
+ pkt = struct.pack(b" self.STATUS_BYTES_LENGTH:
+ return data[: -self.STATUS_BYTES_LENGTH]
+ else:
+ # otherwise, just return the 'val' field which comes from the reply header
+ # (this is used by read_reg)
+ return val
+
+ def flush_input(self):
+ self._port.flushInput()
+ self._slip_reader = slip_reader(self._port, self.trace)
+
+ def sync(self):
+ val, _ = self.command(
+ self.ESP_SYNC, b"\x07\x07\x12\x20" + 32 * b"\x55", timeout=SYNC_TIMEOUT
+ )
+
+ # ROM bootloaders send some non-zero "val" response. The flasher stub sends 0.
+ # If we receive 0 then it probably indicates that the chip wasn't or couldn't be
+ # reset properly and esptool is talking to the flasher stub.
+ self.sync_stub_detected = val == 0
+
+ for _ in range(7):
+ val, _ = self.command()
+ self.sync_stub_detected &= val == 0
+
+ def _get_pid(self):
+ if self.cache["usb_pid"] is not None:
+ return self.cache["usb_pid"]
+
+ if list_ports is None:
+ print(
+ "\nListing all serial ports is currently not available. "
+ "Can't get device PID."
+ )
+ return
+ active_port = self._port.port
+
+ # Pyserial only identifies regular ports, URL handlers are not supported
+ if not active_port.lower().startswith(("com", "/dev/")):
+ print(
+ "\nDevice PID identification is only supported on "
+ "COM and /dev/ serial ports."
+ )
+ return
+ # Return the real path if the active port is a symlink
+ if active_port.startswith("/dev/") and os.path.islink(active_port):
+ active_port = os.path.realpath(active_port)
+
+ active_ports = [active_port]
+
+ # The "cu" (call-up) device has to be used for outgoing communication on MacOS
+ if sys.platform == "darwin" and "tty" in active_port:
+ active_ports.append(active_port.replace("tty", "cu"))
+ ports = list_ports.comports()
+ for p in ports:
+ if p.device in active_ports:
+ self.cache["usb_pid"] = p.pid
+ return p.pid
+ print(
+ f"\nFailed to get PID of a device on {active_port}, "
+ "using standard reset sequence."
+ )
+
+ def _connect_attempt(self, reset_strategy, mode="default_reset"):
+ """A single connection attempt"""
+ last_error = None
+ boot_log_detected = False
+ download_mode = False
+
+ # If we're doing no_sync, we're likely communicating as a pass through
+ # with an intermediate device to the ESP32
+ if mode == "no_reset_no_sync":
+ return last_error
+
+ if mode != "no_reset":
+ if not self.USES_RFC2217: # Might block on rfc2217 ports
+ # Empty serial buffer to isolate boot log
+ self._port.reset_input_buffer()
+
+ reset_strategy() # Reset the chip to bootloader (download mode)
+
+ # Detect the ROM boot log and check actual boot mode (ESP32 and later only)
+ waiting = self._port.inWaiting()
+ read_bytes = self._port.read(waiting)
+ data = re.search(
+ b"boot:(0x[0-9a-fA-F]+)(.*waiting for download)?", read_bytes, re.DOTALL
+ )
+ if data is not None:
+ boot_log_detected = True
+ boot_mode = data.group(1)
+ download_mode = data.group(2) is not None
+
+ for _ in range(5):
+ try:
+ self.flush_input()
+ self._port.flushOutput()
+ self.sync()
+ return None
+ except FatalError as e:
+ print(".", end="")
+ sys.stdout.flush()
+ time.sleep(0.05)
+ last_error = e
+
+ if boot_log_detected:
+ last_error = FatalError(
+ "Wrong boot mode detected ({})! "
+ "The chip needs to be in download mode.".format(
+ boot_mode.decode("utf-8")
+ )
+ )
+ if download_mode:
+ last_error = FatalError(
+ "Download mode successfully detected, but getting no sync reply: "
+ "The serial TX path seems to be down."
+ )
+ return last_error
+
+ def get_memory_region(self, name):
+ """
+ Returns a tuple of (start, end) for the memory map entry with the given name,
+ or None if it doesn't exist
+ """
+ try:
+ return [(start, end) for (start, end, n) in self.MEMORY_MAP if n == name][0]
+ except IndexError:
+ return None
+
+ def _construct_reset_strategy_sequence(self, mode):
+ """
+ Constructs a sequence of reset strategies based on the OS,
+ used ESP chip, external settings, and environment variables.
+ Returns a tuple of one or more reset strategies to be tried sequentially.
+ """
+ cfg_custom_reset_sequence = cfg.get("custom_reset_sequence")
+ if cfg_custom_reset_sequence is not None:
+ return (CustomReset(self._port, cfg_custom_reset_sequence),)
+
+ cfg_reset_delay = cfg.getfloat("reset_delay")
+ if cfg_reset_delay is not None:
+ delay = extra_delay = cfg_reset_delay
+ else:
+ delay = DEFAULT_RESET_DELAY
+ extra_delay = DEFAULT_RESET_DELAY + 0.5
+
+ # This FPGA delay is for Espressif internal use
+ if (
+ self.CHIP_NAME == "ESP32"
+ and os.environ.get("ESPTOOL_ENV_FPGA", "").strip() == "1"
+ ):
+ delay = extra_delay = 7
+
+ # USB-JTAG/Serial mode
+ if mode == "usb_reset" or self._get_pid() == self.USB_JTAG_SERIAL_PID:
+ return (USBJTAGSerialReset(self._port),)
+
+ # USB-to-Serial bridge
+ if os.name != "nt" and not self._port.name.startswith("rfc2217:"):
+ return (
+ UnixTightReset(self._port, delay),
+ UnixTightReset(self._port, extra_delay),
+ ClassicReset(self._port, delay),
+ ClassicReset(self._port, extra_delay),
+ )
+
+ return (
+ ClassicReset(self._port, delay),
+ ClassicReset(self._port, extra_delay),
+ )
+
+ def connect(
+ self,
+ mode="default_reset",
+ attempts=DEFAULT_CONNECT_ATTEMPTS,
+ detecting=False,
+ warnings=True,
+ ):
+ """Try connecting repeatedly until successful, or giving up"""
+ if warnings and mode in ["no_reset", "no_reset_no_sync"]:
+ print(
+ 'WARNING: Pre-connection option "{}" was selected.'.format(mode),
+ "Connection may fail if the chip is not in bootloader "
+ "or flasher stub mode.",
+ )
+
+ if self._port.name.startswith("socket:"):
+ mode = "no_reset" # not possible to toggle DTR/RTS over a TCP socket
+ print(
+ "Note: It's not possible to reset the chip over a TCP socket. "
+ "Automatic resetting to bootloader has been disabled, "
+ "reset the chip manually."
+ )
+
+ print("Connecting...", end="")
+ sys.stdout.flush()
+ last_error = None
+
+ reset_sequence = self._construct_reset_strategy_sequence(mode)
+ try:
+ for _, reset_strategy in zip(
+ range(attempts) if attempts > 0 else itertools.count(),
+ itertools.cycle(reset_sequence),
+ ):
+ last_error = self._connect_attempt(reset_strategy, mode)
+ if last_error is None:
+ break
+ finally:
+ print("") # end 'Connecting...' line
+
+ if last_error is not None:
+ additional_msg = ""
+ if self.CHIP_NAME == "ESP32-C2" and self._port.baudrate < 115200:
+ additional_msg = (
+ "\nNote: Please set a higher baud rate (--baud)"
+ " if ESP32-C2 doesn't connect"
+ " (at least 115200 Bd is recommended)."
+ )
+
+ raise FatalError(
+ "Failed to connect to {}: {}"
+ f"{additional_msg}"
+ "\nFor troubleshooting steps visit: "
+ "https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html".format( # noqa E501
+ self.CHIP_NAME, last_error
+ )
+ )
+
+ if not detecting:
+ try:
+ from .targets import ROM_LIST
+
+ # check the date code registers match what we expect to see
+ chip_magic_value = self.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR)
+ if chip_magic_value not in self.CHIP_DETECT_MAGIC_VALUE:
+ actually = None
+ for cls in ROM_LIST:
+ if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE:
+ actually = cls
+ break
+ if warnings and actually is None:
+ print(
+ "WARNING: This chip doesn't appear to be a %s "
+ "(chip magic value 0x%08x). "
+ "Probably it is unsupported by this version of esptool."
+ % (self.CHIP_NAME, chip_magic_value)
+ )
+ else:
+ raise FatalError(
+ "This chip is %s not %s. Wrong --chip argument?"
+ % (actually.CHIP_NAME, self.CHIP_NAME)
+ )
+ except UnsupportedCommandError:
+ self.secure_download_mode = True
+
+ try:
+ self.check_chip_id()
+ except UnsupportedCommandError:
+ # Fix for ROM not responding in SDM, reconnect and try again
+ if self.secure_download_mode:
+ self._connect_attempt(mode, reset_sequence[0])
+ self.check_chip_id()
+ else:
+ raise
+ self._post_connect()
+
+ def _post_connect(self):
+ """
+ Additional initialization hook, may be overridden by the chip-specific class.
+ Gets called after connect, and after auto-detection.
+ """
+ pass
+
+ def read_reg(self, addr, timeout=DEFAULT_TIMEOUT):
+ """Read memory address in target"""
+ # we don't call check_command here because read_reg() function is called
+ # when detecting chip type, and the way we check for success
+ # (STATUS_BYTES_LENGTH) is different for different chip types (!)
+ val, data = self.command(
+ self.ESP_READ_REG, struct.pack(" 0:
+ # add a dummy write to a date register as an excuse to have a delay
+ command += struct.pack(
+ " stub_start:
+ raise FatalError(
+ "Software loader is resident at 0x%08x-0x%08x. "
+ "Can't load binary at overlapping address range 0x%08x-0x%08x. "
+ "Either change binary loading address, or use the --no-stub "
+ "option to disable the software loader."
+ % (stub_start, stub_end, load_start, load_end)
+ )
+
+ return self.check_command(
+ "enter RAM download mode",
+ self.ESP_MEM_BEGIN,
+ struct.pack(" length:
+ raise FatalError("Read more than expected")
+
+ digest_frame = self.read()
+ if len(digest_frame) != 16:
+ raise FatalError("Expected digest, got: %s" % hexify(digest_frame))
+ expected_digest = hexify(digest_frame).upper()
+ digest = hashlib.md5(data).hexdigest().upper()
+ if digest != expected_digest:
+ raise FatalError(
+ "Digest mismatch: expected %s, got %s" % (expected_digest, digest)
+ )
+ return data
+
+ def flash_spi_attach(self, hspi_arg):
+ """Send SPI attach command to enable the SPI flash pins
+
+ ESP8266 ROM does this when you send flash_begin, ESP32 ROM
+ has it as a SPI command.
+ """
+ # last 3 bytes in ESP_SPI_ATTACH argument are reserved values
+ arg = struct.pack(" 0:
+ self.write_reg(SPI_MOSI_DLEN_REG, mosi_bits - 1)
+ if miso_bits > 0:
+ self.write_reg(SPI_MISO_DLEN_REG, miso_bits - 1)
+ flags = 0
+ if dummy_len > 0:
+ flags |= dummy_len - 1
+ if addr_len > 0:
+ flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT
+ if flags:
+ self.write_reg(SPI_USR1_REG, flags)
+
+ else:
+
+ def set_data_lengths(mosi_bits, miso_bits):
+ SPI_DATA_LEN_REG = SPI_USR1_REG
+ SPI_MOSI_BITLEN_S = 17
+ SPI_MISO_BITLEN_S = 8
+ mosi_mask = 0 if (mosi_bits == 0) else (mosi_bits - 1)
+ miso_mask = 0 if (miso_bits == 0) else (miso_bits - 1)
+ flags = (miso_mask << SPI_MISO_BITLEN_S) | (
+ mosi_mask << SPI_MOSI_BITLEN_S
+ )
+ if dummy_len > 0:
+ flags |= dummy_len - 1
+ if addr_len > 0:
+ flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT
+ self.write_reg(SPI_DATA_LEN_REG, flags)
+
+ # SPI peripheral "command" bitmasks for SPI_CMD_REG
+ SPI_CMD_USR = 1 << 18
+
+ # shift values
+ SPI_USR2_COMMAND_LEN_SHIFT = 28
+ SPI_USR_ADDR_LEN_SHIFT = 26
+
+ if read_bits > 32:
+ raise FatalError(
+ "Reading more than 32 bits back from a SPI flash "
+ "operation is unsupported"
+ )
+ if len(data) > 64:
+ raise FatalError(
+ "Writing more than 64 bytes of data with one SPI "
+ "command is unsupported"
+ )
+
+ data_bits = len(data) * 8
+ old_spi_usr = self.read_reg(SPI_USR_REG)
+ old_spi_usr2 = self.read_reg(SPI_USR2_REG)
+ flags = SPI_USR_COMMAND
+ if read_bits > 0:
+ flags |= SPI_USR_MISO
+ if data_bits > 0:
+ flags |= SPI_USR_MOSI
+ if addr_len > 0:
+ flags |= SPI_USR_ADDR
+ if dummy_len > 0:
+ flags |= SPI_USR_DUMMY
+ set_data_lengths(data_bits, read_bits)
+ self.write_reg(SPI_USR_REG, flags)
+ self.write_reg(
+ SPI_USR2_REG, (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflash_command
+ )
+ if addr_len > 0:
+ if self.SPI_ADDR_REG_MSB:
+ addr = addr << (32 - addr_len)
+ self.write_reg(SPI_ADDR_REG, addr)
+ if data_bits == 0:
+ self.write_reg(SPI_W0_REG, 0) # clear data register before we read it
+ else:
+ data = pad_to(data, 4, b"\00") # pad to 32-bit multiple
+ words = struct.unpack("I" * (len(data) // 4), data)
+ next_reg = SPI_W0_REG
+ for word in words:
+ self.write_reg(next_reg, word)
+ next_reg += 4
+ self.write_reg(SPI_CMD_REG, SPI_CMD_USR)
+
+ def wait_done():
+ for _ in range(10):
+ if (self.read_reg(SPI_CMD_REG) & SPI_CMD_USR) == 0:
+ return
+ raise FatalError("SPI command did not complete in time")
+
+ wait_done()
+
+ status = self.read_reg(SPI_W0_REG)
+ # restore some SPI controller registers
+ self.write_reg(SPI_USR_REG, old_spi_usr)
+ self.write_reg(SPI_USR2_REG, old_spi_usr2)
+ return status
+
+ def read_spiflash_sfdp(self, addr, read_bits):
+ CMD_RDSFDP = 0x5A
+ return self.run_spiflash_command(
+ CMD_RDSFDP, read_bits=read_bits, addr=addr, addr_len=24, dummy_len=8
+ )
+
+ def read_status(self, num_bytes=2):
+ """Read up to 24 bits (num_bytes) of SPI flash status register contents
+ via RDSR, RDSR2, RDSR3 commands
+
+ Not all SPI flash supports all three commands. The upper 1 or 2
+ bytes may be 0xFF.
+ """
+ SPIFLASH_RDSR = 0x05
+ SPIFLASH_RDSR2 = 0x35
+ SPIFLASH_RDSR3 = 0x15
+
+ status = 0
+ shift = 0
+ for cmd in [SPIFLASH_RDSR, SPIFLASH_RDSR2, SPIFLASH_RDSR3][0:num_bytes]:
+ status += self.run_spiflash_command(cmd, read_bits=8) << shift
+ shift += 8
+ return status
+
+ def write_status(self, new_status, num_bytes=2, set_non_volatile=False):
+ """Write up to 24 bits (num_bytes) of new status register
+
+ num_bytes can be 1, 2 or 3.
+
+ Not all flash supports the additional commands to write the
+ second and third byte of the status register. When writing 2
+ bytes, esptool also sends a 16-byte WRSR command (as some
+ flash types use this instead of WRSR2.)
+
+ If the set_non_volatile flag is set, non-volatile bits will
+ be set as well as volatile ones (WREN used instead of WEVSR).
+
+ """
+ SPIFLASH_WRSR = 0x01
+ SPIFLASH_WRSR2 = 0x31
+ SPIFLASH_WRSR3 = 0x11
+ SPIFLASH_WEVSR = 0x50
+ SPIFLASH_WREN = 0x06
+ SPIFLASH_WRDI = 0x04
+
+ enable_cmd = SPIFLASH_WREN if set_non_volatile else SPIFLASH_WEVSR
+
+ # try using a 16-bit WRSR (not supported by all chips)
+ # this may be redundant, but shouldn't hurt
+ if num_bytes == 2:
+ self.run_spiflash_command(enable_cmd)
+ self.run_spiflash_command(SPIFLASH_WRSR, struct.pack(">= 8
+
+ self.run_spiflash_command(SPIFLASH_WRDI)
+
+ def get_crystal_freq(self):
+ """
+ Figure out the crystal frequency from the UART clock divider
+
+ Returns a normalized value in integer MHz (only values 40 or 26 are supported)
+ """
+ # The logic here is:
+ # - We know that our baud rate and the ESP UART baud rate are roughly the same,
+ # or we couldn't communicate
+ # - We can read the UART clock divider register to know how the ESP derives this
+ # from the APB bus frequency
+ # - Multiplying these two together gives us the bus frequency which is either
+ # the crystal frequency (ESP32) or double the crystal frequency (ESP8266).
+ # See the self.XTAL_CLK_DIVIDER parameter for this factor.
+ uart_div = self.read_reg(self.UART_CLKDIV_REG) & self.UART_CLKDIV_MASK
+ est_xtal = (self._port.baudrate * uart_div) / 1e6 / self.XTAL_CLK_DIVIDER
+ if est_xtal > 45:
+ norm_xtal = 48
+ elif est_xtal > 33:
+ norm_xtal = 40
+ else:
+ norm_xtal = 26
+ if abs(norm_xtal - est_xtal) > 1:
+ print(
+ "WARNING: Detected crystal freq %.2fMHz is quite different to "
+ "normalized freq %dMHz. Unsupported crystal in use?"
+ % (est_xtal, norm_xtal)
+ )
+ return norm_xtal
+
+ def hard_reset(self):
+ print("Hard resetting via RTS pin...")
+ HardReset(self._port)()
+
+ def soft_reset(self, stay_in_bootloader):
+ if not self.IS_STUB:
+ if stay_in_bootloader:
+ return # ROM bootloader is already in bootloader!
+ else:
+ # 'run user code' is as close to a soft reset as we can do
+ self.flash_begin(0, 0)
+ self.flash_finish(False)
+ else:
+ if stay_in_bootloader:
+ # soft resetting from the stub loader
+ # will re-load the ROM bootloader
+ self.flash_begin(0, 0)
+ self.flash_finish(True)
+ elif self.CHIP_NAME != "ESP8266":
+ raise FatalError(
+ "Soft resetting is currently only supported on ESP8266"
+ )
+ else:
+ # running user code from stub loader requires some hacks
+ # in the stub loader
+ self.command(self.ESP_RUN_USER_CODE, wait_response=False)
+
+ def check_chip_id(self):
+ try:
+ chip_id = self.get_chip_id()
+ if chip_id != self.IMAGE_CHIP_ID:
+ print(
+ "WARNING: Chip ID {} ({}) doesn't match expected Chip ID {}. "
+ "esptool may not work correctly.".format(
+ chip_id,
+ self.UNSUPPORTED_CHIPS.get(chip_id, "Unknown"),
+ self.IMAGE_CHIP_ID,
+ )
+ )
+ # Try to flash anyways by disabling stub
+ self.stub_is_disabled = True
+ except NotImplementedInROMError:
+ pass
+
+
+def slip_reader(port, trace_function):
+ """Generator to read SLIP packets from a serial port.
+ Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
+
+ Designed to avoid too many calls to serial.read(1), which can bog
+ down on slow systems.
+ """
+
+ def detect_panic_handler(input):
+ """
+ Checks the input bytes for panic handler messages.
+ Raises a FatalError if Guru Meditation or Fatal Exception is found, as both
+ of these are used between different ROM versions.
+ Tries to also parse the error cause (e.g. IllegalInstruction).
+ """
+
+ guru_meditation = (
+ rb"G?uru Meditation Error: (?:Core \d panic'ed \(([a-zA-Z ]*)\))?"
+ )
+ fatal_exception = rb"F?atal exception \(\d+\): (?:([a-zA-Z ]*)?.*epc)?"
+
+ # Search either for Guru Meditation or Fatal Exception
+ data = re.search(
+ rb"".join([rb"(?:", guru_meditation, rb"|", fatal_exception, rb")"]),
+ input,
+ re.DOTALL,
+ )
+ if data is not None:
+ cause = [
+ "({})".format(i.decode("utf-8"))
+ for i in [data.group(1), data.group(2)]
+ if i is not None
+ ]
+ cause = f" {cause[0]}" if len(cause) else ""
+ msg = f"Guru Meditation Error detected{cause}"
+ raise FatalError(msg)
+
+ partial_packet = None
+ in_escape = False
+ successful_slip = False
+ while True:
+ waiting = port.inWaiting()
+ read_bytes = port.read(1 if waiting == 0 else waiting)
+ if read_bytes == b"":
+ if partial_packet is None: # fail due to no data
+ msg = (
+ "Serial data stream stopped: Possible serial noise or corruption."
+ if successful_slip
+ else "No serial data received."
+ )
+ else: # fail during packet transfer
+ msg = "Packet content transfer stopped (received {} bytes)".format(
+ len(partial_packet)
+ )
+ trace_function(msg)
+ raise FatalError(msg)
+ trace_function("Read %d bytes: %s", len(read_bytes), HexFormatter(read_bytes))
+ for b in read_bytes:
+ b = bytes([b])
+ if partial_packet is None: # waiting for packet header
+ if b == b"\xc0":
+ partial_packet = b""
+ else:
+ trace_function("Read invalid data: %s", HexFormatter(read_bytes))
+ remaining_data = port.read(port.inWaiting())
+ trace_function(
+ "Remaining data in serial buffer: %s",
+ HexFormatter(remaining_data),
+ )
+ detect_panic_handler(read_bytes + remaining_data)
+ raise FatalError(
+ "Invalid head of packet (0x%s): "
+ "Possible serial noise or corruption." % hexify(b)
+ )
+ elif in_escape: # part-way through escape sequence
+ in_escape = False
+ if b == b"\xdc":
+ partial_packet += b"\xc0"
+ elif b == b"\xdd":
+ partial_packet += b"\xdb"
+ else:
+ trace_function("Read invalid data: %s", HexFormatter(read_bytes))
+ remaining_data = port.read(port.inWaiting())
+ trace_function(
+ "Remaining data in serial buffer: %s",
+ HexFormatter(remaining_data),
+ )
+ detect_panic_handler(read_bytes + remaining_data)
+ raise FatalError("Invalid SLIP escape (0xdb, 0x%s)" % (hexify(b)))
+ elif b == b"\xdb": # start of escape sequence
+ in_escape = True
+ elif b == b"\xc0": # end of packet
+ trace_function("Received full packet: %s", HexFormatter(partial_packet))
+ yield partial_packet
+ partial_packet = None
+ successful_slip = True
+ else: # normal byte in packet
+ partial_packet += b
+
+
+class HexFormatter(object):
+ """
+ Wrapper class which takes binary data in its constructor
+ and returns a hex string as it's __str__ method.
+
+ This is intended for "lazy formatting" of trace() output
+ in hex format. Avoids overhead (significant on slow computers)
+ of generating long hex strings even if tracing is disabled.
+
+ Note that this doesn't save any overhead if passed as an
+ argument to "%", only when passed to trace()
+
+ If auto_split is set (default), any long line (> 16 bytes) will be
+ printed as separately indented lines, with ASCII decoding at the end
+ of each line.
+ """
+
+ def __init__(self, binary_string, auto_split=True):
+ self._s = binary_string
+ self._auto_split = auto_split
+
+ def __str__(self):
+ if self._auto_split and len(self._s) > 16:
+ result = ""
+ s = self._s
+ while len(s) > 0:
+ line = s[:16]
+ ascii_line = "".join(
+ (
+ c
+ if (
+ c == " "
+ or (c in string.printable and c not in string.whitespace)
+ )
+ else "."
+ )
+ for c in line.decode("ascii", "replace")
+ )
+ s = s[16:]
+ result += "\n %-16s %-16s | %s" % (
+ hexify(line[:8], False),
+ hexify(line[8:], False),
+ ascii_line,
+ )
+ return result
+ else:
+ return hexify(self._s, False)
diff --git a/mixly/tools/python/esptool/reset.py b/mixly/tools/python/esptool/reset.py
new file mode 100644
index 00000000..ef91e4bd
--- /dev/null
+++ b/mixly/tools/python/esptool/reset.py
@@ -0,0 +1,209 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import errno
+import os
+import struct
+import time
+
+from .util import FatalError, PrintOnce
+
+# Used for resetting into bootloader on Unix-like systems
+if os.name != "nt":
+ import fcntl
+ import termios
+
+ # Constants used for terminal status lines reading/setting.
+ # Taken from pySerial's backend for IO:
+ # https://github.com/pyserial/pyserial/blob/master/serial/serialposix.py
+ TIOCMSET = getattr(termios, "TIOCMSET", 0x5418)
+ TIOCMGET = getattr(termios, "TIOCMGET", 0x5415)
+ TIOCM_DTR = getattr(termios, "TIOCM_DTR", 0x002)
+ TIOCM_RTS = getattr(termios, "TIOCM_RTS", 0x004)
+
+DEFAULT_RESET_DELAY = 0.05 # default time to wait before releasing boot pin after reset
+
+
+class ResetStrategy(object):
+ print_once = PrintOnce()
+
+ def __init__(self, port, reset_delay=DEFAULT_RESET_DELAY):
+ self.port = port
+ self.reset_delay = reset_delay
+
+ def __call__(self):
+ """
+ On targets with USB modes, the reset process can cause the port to
+ disconnect / reconnect during reset.
+ This will retry reconnections on ports that
+ drop out during the reset sequence.
+ """
+ for retry in reversed(range(3)):
+ try:
+ if not self.port.isOpen():
+ self.port.open()
+ self.reset()
+ break
+ except OSError as e:
+ # ENOTTY for TIOCMSET; EINVAL for TIOCMGET
+ if e.errno in [errno.ENOTTY, errno.EINVAL]:
+ self.print_once(
+ "WARNING: Chip was NOT reset. Setting RTS/DTR lines is not "
+ f"supported for port '{self.port.name}'. Set --before and --after "
+ "arguments to 'no_reset' and switch to bootloader manually to "
+ "avoid this warning."
+ )
+ break
+ elif not retry:
+ raise
+ self.port.close()
+ time.sleep(0.5)
+
+ def reset(self):
+ pass
+
+ def _setDTR(self, state):
+ self.port.setDTR(state)
+
+ def _setRTS(self, state):
+ self.port.setRTS(state)
+ # Work-around for adapters on Windows using the usbser.sys driver:
+ # generate a dummy change to DTR so that the set-control-line-state
+ # request is sent with the updated RTS state and the same DTR state
+ self.port.setDTR(self.port.dtr)
+
+ def _setDTRandRTS(self, dtr=False, rts=False):
+ status = struct.unpack(
+ "I", fcntl.ioctl(self.port.fileno(), TIOCMGET, struct.pack("I", 0))
+ )[0]
+ if dtr:
+ status |= TIOCM_DTR
+ else:
+ status &= ~TIOCM_DTR
+ if rts:
+ status |= TIOCM_RTS
+ else:
+ status &= ~TIOCM_RTS
+ fcntl.ioctl(self.port.fileno(), TIOCMSET, struct.pack("I", status))
+
+
+class ClassicReset(ResetStrategy):
+ """
+ Classic reset sequence, sets DTR and RTS lines sequentially.
+ """
+
+ def reset(self):
+ self._setDTR(False) # IO0=HIGH
+ self._setRTS(True) # EN=LOW, chip in reset
+ time.sleep(0.1)
+ self._setDTR(True) # IO0=LOW
+ self._setRTS(False) # EN=HIGH, chip out of reset
+ time.sleep(self.reset_delay)
+ self._setDTR(False) # IO0=HIGH, done
+
+
+class UnixTightReset(ResetStrategy):
+ """
+ UNIX-only reset sequence with custom implementation,
+ which allows setting DTR and RTS lines at the same time.
+ """
+
+ def reset(self):
+ self._setDTRandRTS(False, False)
+ self._setDTRandRTS(True, True)
+ self._setDTRandRTS(False, True) # IO0=HIGH & EN=LOW, chip in reset
+ time.sleep(0.1)
+ self._setDTRandRTS(True, False) # IO0=LOW & EN=HIGH, chip out of reset
+ time.sleep(self.reset_delay)
+ self._setDTRandRTS(False, False) # IO0=HIGH, done
+ self._setDTR(False) # Needed in some environments to ensure IO0=HIGH
+
+
+class USBJTAGSerialReset(ResetStrategy):
+ """
+ Custom reset sequence, which is required when the device
+ is connecting via its USB-JTAG-Serial peripheral.
+ """
+
+ def reset(self):
+ self._setRTS(False)
+ self._setDTR(False) # Idle
+ time.sleep(0.1)
+ self._setDTR(True) # Set IO0
+ self._setRTS(False)
+ time.sleep(0.1)
+ self._setRTS(True) # Reset. Calls inverted to go through (1,1) instead of (0,0)
+ self._setDTR(False)
+ self._setRTS(True) # RTS set as Windows only propagates DTR on RTS setting
+ time.sleep(0.1)
+ self._setDTR(False)
+ self._setRTS(False) # Chip out of reset
+
+
+class HardReset(ResetStrategy):
+ """
+ Reset sequence for hard resetting the chip.
+ Can be used to reset out of the bootloader or to restart a running app.
+ """
+
+ def __init__(self, port, uses_usb=False):
+ super().__init__(port)
+ self.uses_usb = uses_usb
+
+ def reset(self):
+ self._setRTS(True) # EN->LOW
+ if self.uses_usb:
+ # Give the chip some time to come out of reset,
+ # to be able to handle further DTR/RTS transitions
+ time.sleep(0.2)
+ self._setRTS(False)
+ time.sleep(0.2)
+ else:
+ time.sleep(0.1)
+ self._setRTS(False)
+
+
+class CustomReset(ResetStrategy):
+ """
+ Custom reset strategy defined with a string.
+
+ CustomReset object is created as "rst = CustomReset(port, seq_str)"
+ and can be later executed simply with "rst()"
+
+ The seq_str input string consists of individual commands divided by "|".
+ Commands (e.g. R0) are defined by a code (R) and an argument (0).
+
+ The commands are:
+ D: setDTR - 1=True / 0=False
+ R: setRTS - 1=True / 0=False
+ U: setDTRandRTS (Unix-only) - 0,0 / 0,1 / 1,0 / or 1,1
+ W: Wait (time delay) - positive float number
+
+ e.g.
+ "D0|R1|W0.1|D1|R0|W0.05|D0" represents the ClassicReset strategy
+ "U1,1|U0,1|W0.1|U1,0|W0.05|U0,0" represents the UnixTightReset strategy
+ """
+
+ format_dict = {
+ "D": "self.port.setDTR({})",
+ "R": "self.port.setRTS({})",
+ "W": "time.sleep({})",
+ "U": "self._setDTRandRTS({})",
+ }
+
+ def reset(self):
+ exec(self.constructed_strategy)
+
+ def __init__(self, port, seq_str):
+ super().__init__(port)
+ self.constructed_strategy = self._parse_string_to_seq(seq_str)
+
+ def _parse_string_to_seq(self, seq_str):
+ try:
+ cmds = seq_str.split("|")
+ fn_calls_list = [self.format_dict[cmd[0]].format(cmd[1:]) for cmd in cmds]
+ except Exception as e:
+ raise FatalError(f'Invalid "custom_reset_sequence" option format: {e}')
+ return "\n".join(fn_calls_list)
diff --git a/mixly/tools/python/esptool/targets/__init__.py b/mixly/tools/python/esptool/targets/__init__.py
new file mode 100644
index 00000000..30c08605
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/__init__.py
@@ -0,0 +1,39 @@
+from .esp32 import ESP32ROM
+from .esp32c2 import ESP32C2ROM
+from .esp32c3 import ESP32C3ROM
+from .esp32c5 import ESP32C5ROM
+from .esp32c5beta3 import ESP32C5BETA3ROM
+from .esp32c6 import ESP32C6ROM
+from .esp32c61 import ESP32C61ROM
+from .esp32c6beta import ESP32C6BETAROM
+from .esp32h2 import ESP32H2ROM
+from .esp32h2beta1 import ESP32H2BETA1ROM
+from .esp32h2beta2 import ESP32H2BETA2ROM
+from .esp32p4 import ESP32P4ROM
+from .esp32s2 import ESP32S2ROM
+from .esp32s3 import ESP32S3ROM
+from .esp32s3beta2 import ESP32S3BETA2ROM
+from .esp8266 import ESP8266ROM
+
+
+CHIP_DEFS = {
+ "esp8266": ESP8266ROM,
+ "esp32": ESP32ROM,
+ "esp32s2": ESP32S2ROM,
+ "esp32s3beta2": ESP32S3BETA2ROM,
+ "esp32s3": ESP32S3ROM,
+ "esp32c3": ESP32C3ROM,
+ "esp32c6beta": ESP32C6BETAROM,
+ "esp32h2beta1": ESP32H2BETA1ROM,
+ "esp32h2beta2": ESP32H2BETA2ROM,
+ "esp32c2": ESP32C2ROM,
+ "esp32c6": ESP32C6ROM,
+ "esp32c61": ESP32C61ROM,
+ "esp32c5": ESP32C5ROM,
+ "esp32c5beta3": ESP32C5BETA3ROM,
+ "esp32h2": ESP32H2ROM,
+ "esp32p4": ESP32P4ROM,
+}
+
+CHIP_LIST = list(CHIP_DEFS.keys())
+ROM_LIST = list(CHIP_DEFS.values())
diff --git a/mixly/tools/python/esptool/targets/esp32.py b/mixly/tools/python/esptool/targets/esp32.py
new file mode 100644
index 00000000..aef531a0
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp32.py
@@ -0,0 +1,473 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import struct
+import time
+from typing import Dict, Optional
+
+from ..loader import ESPLoader
+from ..util import FatalError, NotSupportedError
+
+
+class ESP32ROM(ESPLoader):
+ """Access class for ESP32 ROM bootloader"""
+
+ CHIP_NAME = "ESP32"
+ IMAGE_CHIP_ID = 0
+ IS_STUB = False
+
+ CHIP_DETECT_MAGIC_VALUE = [0x00F01D83]
+
+ IROM_MAP_START = 0x400D0000
+ IROM_MAP_END = 0x40400000
+
+ DROM_MAP_START = 0x3F400000
+ DROM_MAP_END = 0x3F800000
+
+ # ESP32 uses a 4 byte status reply
+ STATUS_BYTES_LENGTH = 4
+
+ SPI_REG_BASE = 0x3FF42000
+ SPI_USR_OFFS = 0x1C
+ SPI_USR1_OFFS = 0x20
+ SPI_USR2_OFFS = 0x24
+ SPI_MOSI_DLEN_OFFS = 0x28
+ SPI_MISO_DLEN_OFFS = 0x2C
+ EFUSE_RD_REG_BASE = 0x3FF5A000
+
+ EFUSE_BLK0_RDATA3_REG_OFFS = EFUSE_RD_REG_BASE + 0x00C
+ EFUSE_BLK0_RDATA5_REG_OFFS = EFUSE_RD_REG_BASE + 0x014
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + 0x18
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 7 # EFUSE_RD_DISABLE_DL_ENCRYPT
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_RD_REG_BASE # EFUSE_BLK0_WDATA0_REG
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7F << 20 # EFUSE_FLASH_CRYPT_CNT
+
+ EFUSE_RD_ABS_DONE_REG = EFUSE_RD_REG_BASE + 0x018
+ EFUSE_RD_ABS_DONE_0_MASK = 1 << 4
+ EFUSE_RD_ABS_DONE_1_MASK = 1 << 5
+
+ EFUSE_VDD_SPI_REG = EFUSE_RD_REG_BASE + 0x10
+ VDD_SPI_XPD = 1 << 14 # XPD_SDIO_REG
+ VDD_SPI_TIEH = 1 << 15 # XPD_SDIO_TIEH
+ VDD_SPI_FORCE = 1 << 16 # XPD_SDIO_FORCE
+
+ DR_REG_SYSCON_BASE = 0x3FF66000
+ APB_CTL_DATE_ADDR = DR_REG_SYSCON_BASE + 0x7C
+ APB_CTL_DATE_V = 0x1
+ APB_CTL_DATE_S = 31
+
+ SPI_W0_OFFS = 0x80
+
+ UART_CLKDIV_REG = 0x3FF40014
+
+ XTAL_CLK_DIVIDER = 1
+
+ RTCCALICFG1 = 0x3FF5F06C
+ TIMERS_RTC_CALI_VALUE = 0x01FFFFFF
+ TIMERS_RTC_CALI_VALUE_S = 7
+
+ GPIO_STRAP_REG = 0x3FF44038
+ GPIO_STRAP_VDDSPI_MASK = 1 << 5 # GPIO_STRAP_VDDSDIO
+
+ RTC_CNTL_SDIO_CONF_REG = 0x3FF48074
+ RTC_CNTL_XPD_SDIO_REG = 1 << 31
+ RTC_CNTL_DREFH_SDIO_M = 3 << 29
+ RTC_CNTL_DREFM_SDIO_M = 3 << 27
+ RTC_CNTL_DREFL_SDIO_M = 3 << 25
+ RTC_CNTL_SDIO_FORCE = 1 << 22
+ RTC_CNTL_SDIO_PD_EN = 1 << 21
+
+ FLASH_SIZES = {
+ "1MB": 0x00,
+ "2MB": 0x10,
+ "4MB": 0x20,
+ "8MB": 0x30,
+ "16MB": 0x40,
+ "32MB": 0x50,
+ "64MB": 0x60,
+ "128MB": 0x70,
+ }
+
+ FLASH_FREQUENCY = {
+ "80m": 0xF,
+ "40m": 0x0,
+ "26m": 0x1,
+ "20m": 0x2,
+ }
+
+ BOOTLOADER_FLASH_OFFSET = 0x1000
+
+ OVERRIDE_VDDSDIO_CHOICES = ["1.8V", "1.9V", "OFF"]
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x3F400000, 0x3F800000, "DROM"],
+ [0x3F800000, 0x3FC00000, "EXTRAM_DATA"],
+ [0x3FF80000, 0x3FF82000, "RTC_DRAM"],
+ [0x3FF90000, 0x40000000, "BYTE_ACCESSIBLE"],
+ [0x3FFAE000, 0x40000000, "DRAM"],
+ [0x3FFE0000, 0x3FFFFFFC, "DIRAM_DRAM"],
+ [0x40000000, 0x40070000, "IROM"],
+ [0x40070000, 0x40078000, "CACHE_PRO"],
+ [0x40078000, 0x40080000, "CACHE_APP"],
+ [0x40080000, 0x400A0000, "IRAM"],
+ [0x400A0000, 0x400BFFFC, "DIRAM_IRAM"],
+ [0x400C0000, 0x400C2000, "RTC_IRAM"],
+ [0x400D0000, 0x40400000, "IROM"],
+ [0x50000000, 0x50002000, "RTC_DATA"],
+ ]
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 32
+
+ UF2_FAMILY_ID = 0x1C5F21B0
+
+ KEY_PURPOSES: Dict[int, str] = {}
+
+ """ Try to read the BLOCK1 (encryption key) and check if it is valid """
+
+ def is_flash_encryption_key_valid(self):
+ """Bit 0 of efuse_rd_disable[3:0] is mapped to BLOCK1
+ this bit is at position 16 in EFUSE_BLK0_RDATA0_REG"""
+ word0 = self.read_efuse(0)
+ rd_disable = (word0 >> 16) & 0x1
+
+ # reading of BLOCK1 is NOT ALLOWED so we assume valid key is programmed
+ if rd_disable:
+ return True
+ else:
+ # reading of BLOCK1 is ALLOWED so we will read and verify for non-zero.
+ # When ESP32 has not generated AES/encryption key in BLOCK1,
+ # the contents will be readable and 0.
+ # If the flash encryption is enabled it is expected to have a valid
+ # non-zero key. We break out on first occurrence of non-zero value
+ key_word = [0] * 7
+ for i in range(len(key_word)):
+ key_word[i] = self.read_efuse(14 + i)
+ # key is non-zero so break & return
+ if key_word[i] != 0:
+ return True
+ return False
+
+ def get_flash_crypt_config(self):
+ """For flash encryption related commands we need to make sure
+ user has programmed all the relevant efuse correctly so before
+ writing encrypted write_flash_encrypt esptool will verify the values
+ of flash_crypt_config to be non zero if they are not read
+ protected. If the values are zero a warning will be printed
+
+ bit 3 in efuse_rd_disable[3:0] is mapped to flash_crypt_config
+ this bit is at position 19 in EFUSE_BLK0_RDATA0_REG"""
+ word0 = self.read_efuse(0)
+ rd_disable = (word0 >> 19) & 0x1
+
+ if rd_disable == 0:
+ """we can read the flash_crypt_config efuse value
+ so go & read it (EFUSE_BLK0_RDATA5_REG[31:28])"""
+ word5 = self.read_efuse(5)
+ word5 = (word5 >> 28) & 0xF
+ return word5
+ else:
+ # if read of the efuse is disabled we assume it is set correctly
+ return 0xF
+
+ def get_encrypted_download_disabled(self):
+ return (
+ self.read_reg(self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG)
+ & self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT
+ )
+
+ def get_flash_encryption_enabled(self):
+ flash_crypt_cnt = (
+ self.read_reg(self.EFUSE_SPI_BOOT_CRYPT_CNT_REG)
+ & self.EFUSE_SPI_BOOT_CRYPT_CNT_MASK
+ )
+ # Flash encryption enabled when odd number of bits are set
+ return bin(flash_crypt_cnt).count("1") & 1 != 0
+
+ def get_secure_boot_enabled(self):
+ efuses = self.read_reg(self.EFUSE_RD_ABS_DONE_REG)
+ rev = self.get_chip_revision()
+ return efuses & self.EFUSE_RD_ABS_DONE_0_MASK or (
+ rev >= 300 and efuses & self.EFUSE_RD_ABS_DONE_1_MASK
+ )
+
+ def get_pkg_version(self):
+ word3 = self.read_efuse(3)
+ pkg_version = (word3 >> 9) & 0x07
+ pkg_version += ((word3 >> 2) & 0x1) << 3
+ return pkg_version
+
+ def get_chip_revision(self):
+ return self.get_major_chip_version() * 100 + self.get_minor_chip_version()
+
+ def get_minor_chip_version(self):
+ return (self.read_efuse(5) >> 24) & 0x3
+
+ def get_major_chip_version(self):
+ rev_bit0 = (self.read_efuse(3) >> 15) & 0x1
+ rev_bit1 = (self.read_efuse(5) >> 20) & 0x1
+ apb_ctl_date = self.read_reg(self.APB_CTL_DATE_ADDR)
+ rev_bit2 = (apb_ctl_date >> self.APB_CTL_DATE_S) & self.APB_CTL_DATE_V
+ combine_value = (rev_bit2 << 2) | (rev_bit1 << 1) | rev_bit0
+
+ revision = {
+ 0: 0,
+ 1: 1,
+ 3: 2,
+ 7: 3,
+ }.get(combine_value, 0)
+ return revision
+
+ def get_chip_description(self):
+ pkg_version = self.get_pkg_version()
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ rev3 = major_rev == 3
+ sc = self.read_efuse(3) & (1 << 0) # single core, CHIP_VER DIS_APP_CPU
+
+ chip_name = {
+ 0: "ESP32-S0WDQ6" if sc else "ESP32-D0WDQ6-V3" if rev3 else "ESP32-D0WDQ6",
+ 1: "ESP32-S0WD" if sc else "ESP32-D0WD-V3" if rev3 else "ESP32-D0WD",
+ 2: "ESP32-D2WD",
+ 4: "ESP32-U4WDH",
+ 5: "ESP32-PICO-V3" if rev3 else "ESP32-PICO-D4",
+ 6: "ESP32-PICO-V3-02",
+ 7: "ESP32-D0WDR2-V3",
+ }.get(pkg_version, "unknown ESP32")
+
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ features = ["WiFi"]
+ word3 = self.read_efuse(3)
+
+ # names of variables in this section are lowercase
+ # versions of EFUSE names as documented in TRM and
+ # ESP-IDF efuse_reg.h
+
+ chip_ver_dis_bt = word3 & (1 << 1)
+ if chip_ver_dis_bt == 0:
+ features += ["BT"]
+
+ chip_ver_dis_app_cpu = word3 & (1 << 0)
+ if chip_ver_dis_app_cpu:
+ features += ["Single Core"]
+ else:
+ features += ["Dual Core"]
+
+ chip_cpu_freq_rated = word3 & (1 << 13)
+ if chip_cpu_freq_rated:
+ chip_cpu_freq_low = word3 & (1 << 12)
+ if chip_cpu_freq_low:
+ features += ["160MHz"]
+ else:
+ features += ["240MHz"]
+
+ pkg_version = self.get_pkg_version()
+ if pkg_version in [2, 4, 5, 6]:
+ features += ["Embedded Flash"]
+
+ if pkg_version == 6:
+ features += ["Embedded PSRAM"]
+
+ word4 = self.read_efuse(4)
+ adc_vref = (word4 >> 8) & 0x1F
+ if adc_vref:
+ features += ["VRef calibration in efuse"]
+
+ blk3_part_res = word3 >> 14 & 0x1
+ if blk3_part_res:
+ features += ["BLK3 partially reserved"]
+
+ word6 = self.read_efuse(6)
+ coding_scheme = word6 & 0x3
+ features += [
+ "Coding Scheme %s"
+ % {
+ 0: "None",
+ 1: "3/4",
+ 2: "Repeat (UNSUPPORTED)",
+ 3: "None (may contain encoding data)",
+ }[coding_scheme]
+ ]
+
+ return features
+
+ def get_chip_spi_pads(self):
+ """Read chip spi pad config
+ return: clk, q, d, hd, cd
+ """
+ efuse_blk0_rdata5 = self.read_reg(self.EFUSE_BLK0_RDATA5_REG_OFFS)
+ spi_pad_clk = efuse_blk0_rdata5 & 0x1F
+ spi_pad_q = (efuse_blk0_rdata5 >> 5) & 0x1F
+ spi_pad_d = (efuse_blk0_rdata5 >> 10) & 0x1F
+ spi_pad_cs = (efuse_blk0_rdata5 >> 15) & 0x1F
+
+ efuse_blk0_rdata3_reg = self.read_reg(self.EFUSE_BLK0_RDATA3_REG_OFFS)
+ spi_pad_hd = (efuse_blk0_rdata3_reg >> 4) & 0x1F
+ return spi_pad_clk, spi_pad_q, spi_pad_d, spi_pad_hd, spi_pad_cs
+
+ def read_efuse(self, n):
+ """Read the nth word of the ESP3x EFUSE region."""
+ return self.read_reg(self.EFUSE_RD_REG_BASE + (4 * n))
+
+ def chip_id(self):
+ raise NotSupportedError(self, "Function chip_id")
+
+ def read_mac(self, mac_type="BASE_MAC"):
+ """Read MAC from EFUSE region"""
+ if mac_type != "BASE_MAC":
+ return None
+ words = [self.read_efuse(2), self.read_efuse(1)]
+ bitstring = struct.pack(">II", *words)
+ bitstring = bitstring[2:8] # trim the 2 byte CRC
+ return tuple(bitstring)
+
+ def get_erase_size(self, offset, size):
+ return size
+
+ def _get_efuse_flash_voltage(self) -> Optional[str]:
+ efuse = self.read_reg(self.EFUSE_VDD_SPI_REG)
+ # check efuse setting
+ if efuse & (self.VDD_SPI_FORCE | self.VDD_SPI_XPD | self.VDD_SPI_TIEH):
+ return "3.3V"
+ elif efuse & (self.VDD_SPI_FORCE | self.VDD_SPI_XPD):
+ return "1.8V"
+ elif efuse & self.VDD_SPI_FORCE:
+ return "OFF"
+ return None
+
+ def _get_rtc_cntl_flash_voltage(self) -> Optional[str]:
+ reg = self.read_reg(self.RTC_CNTL_SDIO_CONF_REG)
+ # check if override is set in RTC_CNTL_SDIO_CONF_REG
+ if reg & self.RTC_CNTL_SDIO_FORCE:
+ if reg & self.RTC_CNTL_DREFH_SDIO_M:
+ return "1.9V"
+ elif reg & self.RTC_CNTL_XPD_SDIO_REG:
+ return "1.8V"
+ else:
+ return "OFF"
+ return None
+
+ def get_flash_voltage(self):
+ """Get flash voltage setting and print it to the console."""
+ voltage = self._get_rtc_cntl_flash_voltage()
+ source = "RTC_CNTL"
+ if not voltage:
+ voltage = self._get_efuse_flash_voltage()
+ source = "eFuse"
+ if not voltage:
+ strap_reg = self.read_reg(self.GPIO_STRAP_REG)
+ strap_reg &= self.GPIO_STRAP_VDDSPI_MASK
+ voltage = "1.8V" if strap_reg else "3.3V"
+ source = "a strapping pin"
+ print(f"Flash voltage set by {source} to {voltage}")
+
+ def override_vddsdio(self, new_voltage):
+ new_voltage = new_voltage.upper()
+ if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES:
+ raise FatalError(
+ f"The only accepted VDDSDIO overrides are {', '.join(self.OVERRIDE_VDDSDIO_CHOICES)}"
+ )
+ # RTC_CNTL_SDIO_TIEH is not used here, setting TIEH=1 would set 3.3V output,
+ # not safe for esptool.py to do
+
+ reg_val = self.RTC_CNTL_SDIO_FORCE # override efuse setting
+ reg_val |= self.RTC_CNTL_SDIO_PD_EN
+ if new_voltage != "OFF":
+ reg_val |= self.RTC_CNTL_XPD_SDIO_REG # enable internal LDO
+ if new_voltage == "1.9V":
+ reg_val |= (
+ self.RTC_CNTL_DREFH_SDIO_M
+ | self.RTC_CNTL_DREFM_SDIO_M
+ | self.RTC_CNTL_DREFL_SDIO_M
+ ) # boost voltage
+ self.write_reg(self.RTC_CNTL_SDIO_CONF_REG, reg_val)
+ print("VDDSDIO regulator set to %s" % new_voltage)
+
+ def read_flash_slow(self, offset, length, progress_fn):
+ BLOCK_LEN = 64 # ROM read limit per command (this limit is why it's so slow)
+
+ data = b""
+ while len(data) < length:
+ block_len = min(BLOCK_LEN, length - len(data))
+ try:
+ r = self.check_command(
+ "read flash block",
+ self.ESP_READ_FLASH_SLOW,
+ struct.pack("> self.TIMERS_RTC_CALI_VALUE_S
+ ) & self.TIMERS_RTC_CALI_VALUE
+ clk_8M_freq = self.read_efuse(4) & (0xFF) # EFUSE_RD_CK8M_FREQ
+ rom_calculated_freq = cali_val * 15625 * clk_8M_freq / 40
+ return rom_calculated_freq
+
+ def change_baud(self, baud):
+ assert self.CHIP_NAME == "ESP32", "This workaround should only apply to ESP32"
+ # It's a workaround to avoid esp32 CK_8M frequency drift.
+ rom_calculated_freq = self.get_rom_cal_crystal_freq()
+ valid_freq = 40000000 if rom_calculated_freq > 33000000 else 26000000
+ false_rom_baud = int(baud * rom_calculated_freq // valid_freq)
+
+ print(f"Changing baud rate to {baud}")
+ self.command(self.ESP_CHANGE_BAUDRATE, struct.pack("> 22) & 0x07
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C2",
+ 1: "ESP32-C2",
+ }.get(self.get_pkg_version(), "unknown ESP32-C2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_minor_chip_version(self):
+ num_word = 1
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 16) & 0xF
+
+ def get_major_chip_version(self):
+ num_word = 1
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 20) & 0x3
+
+ def get_flash_cap(self):
+ # ESP32-C2 doesn't have eFuse field FLASH_CAP.
+ # Can't get info about the flash chip.
+ return 0
+
+ def get_flash_vendor(self):
+ # ESP32-C2 doesn't have eFuse field FLASH_VENDOR.
+ # Can't get info about the flash chip.
+ return ""
+
+ def get_crystal_freq(self):
+ # The crystal detection algorithm of ESP32/ESP8266 works for ESP32-C2 as well.
+ return ESPLoader.get_crystal_freq(self)
+
+ def change_baud(self, baud):
+ rom_with_26M_XTAL = not self.IS_STUB and self.get_crystal_freq() == 26
+ if rom_with_26M_XTAL:
+ # The code is copied over from ESPLoader.change_baud().
+ # Probably this is just a temporary solution until the next chip revision.
+
+ # The ROM code thinks it uses a 40 MHz XTAL. Recompute the baud rate
+ # in order to trick the ROM code to set the correct baud rate for
+ # a 26 MHz XTAL.
+ false_rom_baud = baud * 40 // 26
+
+ print(f"Changing baud rate to {baud}")
+ self.command(
+ self.ESP_CHANGE_BAUDRATE, struct.pack("> 21) & 0x07
+
+ def get_minor_chip_version(self):
+ hi_num_word = 5
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+ low_num_word = 3
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+ return (hi << 3) + low
+
+ def get_major_chip_version(self):
+ num_word = 5
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
+
+ def get_flash_cap(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 27) & 0x07
+
+ def get_flash_vendor(self):
+ num_word = 4
+ vendor_id = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x07
+ return {1: "XMC", 2: "GD", 3: "FM", 4: "TT", 5: "ZBIT"}.get(vendor_id, "")
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C3 (QFN32)",
+ 1: "ESP8685 (QFN28)",
+ 2: "ESP32-C3 AZ (QFN32)",
+ 3: "ESP8686 (QFN24)",
+ }.get(self.get_pkg_version(), "unknown ESP32-C3")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ features = ["WiFi", "BLE"]
+
+ flash = {
+ 0: None,
+ 1: "Embedded Flash 4MB",
+ 2: "Embedded Flash 2MB",
+ 3: "Embedded Flash 1MB",
+ 4: "Embedded Flash 8MB",
+ }.get(self.get_flash_cap(), "Unknown Embedded Flash")
+ if flash is not None:
+ features += [flash + f" ({self.get_flash_vendor()})"]
+ return features
+
+ def get_crystal_freq(self):
+ # ESP32C3 XTAL is fixed to 40MHz
+ return 40
+
+ def get_flash_voltage(self):
+ pass # not supported on ESP32-C3
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-C3"
+ )
+
+ def read_mac(self, mac_type="BASE_MAC"):
+ """Read MAC from EFUSE region"""
+ if mac_type != "BASE_MAC":
+ return None
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-C3
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > self.EFUSE_MAX_KEY:
+ raise FatalError(
+ f"Valid key block numbers must be in range 0-{self.EFUSE_MAX_KEY}"
+ )
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see an AES-128 key
+ purposes = [
+ self.get_key_block_purpose(b) for b in range(self.EFUSE_MAX_KEY + 1)
+ ]
+
+ return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
+
+ def change_baud(self, baud):
+ ESPLoader.change_baud(self, baud)
+
+ def uses_usb_jtag_serial(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-JTAG/Serial is being used
+ """
+ if self.secure_download_mode:
+ return False # Can't detect USB-JTAG/Serial in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_JTAG_SERIAL
+
+ def disable_watchdogs(self):
+ # When USB-JTAG/Serial is used, the RTC WDT and SWD watchdog are not reset
+ # and can then reset the board during flashing. Disable or autofeed them.
+ if self.uses_usb_jtag_serial():
+ # Disable RTC WDT
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY)
+ self.write_reg(self.RTC_CNTL_WDTCONFIG0_REG, 0)
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0)
+
+ # Automatically feed SWD
+ self.write_reg(self.RTC_CNTL_SWD_WPROTECT_REG, self.RTC_CNTL_SWD_WKEY)
+ self.write_reg(
+ self.RTC_CNTL_SWD_CONF_REG,
+ self.read_reg(self.RTC_CNTL_SWD_CONF_REG)
+ | self.RTC_CNTL_SWD_AUTO_FEED_EN,
+ )
+ self.write_reg(self.RTC_CNTL_SWD_WPROTECT_REG, 0)
+
+ def _post_connect(self):
+ if not self.sync_stub_detected: # Don't run if stub is reused
+ self.disable_watchdogs()
+
+ def check_spi_connection(self, spi_connection):
+ if not set(spi_connection).issubset(set(range(0, 22))):
+ raise FatalError("SPI Pin numbers must be in the range 0-21.")
+ if any([v for v in spi_connection if v in [18, 19]]):
+ print(
+ "WARNING: GPIO pins 18 and 19 are used by USB-Serial/JTAG, "
+ "consider using other pins for SPI flash connection."
+ )
+
+
+class ESP32C3StubLoader(ESP32C3ROM):
+ """Access class for ESP32C3 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32C3ROM.STUB_CLASS = ESP32C3StubLoader
diff --git a/mixly/tools/python/esptool/targets/esp32c5.py b/mixly/tools/python/esptool/targets/esp32c5.py
new file mode 100644
index 00000000..99c9b99d
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp32c5.py
@@ -0,0 +1,190 @@
+# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import struct
+import time
+from typing import Dict
+
+from .esp32c6 import ESP32C6ROM
+from ..loader import ESPLoader
+from ..reset import HardReset
+from ..util import FatalError
+
+
+class ESP32C5ROM(ESP32C6ROM):
+ CHIP_NAME = "ESP32-C5"
+ IMAGE_CHIP_ID = 23
+
+ EFUSE_BASE = 0x600B4800
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+ MAC_EFUSE_REG = EFUSE_BASE + 0x044
+
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ IROM_MAP_START = 0x42000000
+ IROM_MAP_END = 0x42800000
+ DROM_MAP_START = 0x42800000
+ DROM_MAP_END = 0x43000000
+
+ PCR_SYSCLK_CONF_REG = 0x60096110
+ PCR_SYSCLK_XTAL_FREQ_V = 0x7F << 24
+ PCR_SYSCLK_XTAL_FREQ_S = 24
+
+ UARTDEV_BUF_NO = 0x4085F51C # Variable in ROM .bss which indicates the port in use
+
+ # Magic value for ESP32C5
+ CHIP_DETECT_MAGIC_VALUE = [0x1101406F]
+
+ FLASH_FREQUENCY = {
+ "80m": 0xF,
+ "40m": 0x0,
+ "20m": 0x2,
+ }
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x42800000, 0x43000000, "DROM"],
+ [0x40800000, 0x40860000, "DRAM"],
+ [0x40800000, 0x40860000, "BYTE_ACCESSIBLE"],
+ [0x4003A000, 0x40040000, "DROM_MASK"],
+ [0x40000000, 0x4003A000, "IROM_MASK"],
+ [0x42000000, 0x42800000, "IROM"],
+ [0x40800000, 0x40860000, "IRAM"],
+ [0x50000000, 0x50004000, "RTC_IRAM"],
+ [0x50000000, 0x50004000, "RTC_DRAM"],
+ [0x600FE000, 0x60100000, "MEM_INTERNAL2"],
+ ]
+
+ UF2_FAMILY_ID = 0xF71C0343
+
+ EFUSE_MAX_KEY = 5
+ KEY_PURPOSES: Dict[int, str] = {
+ 0: "USER/EMPTY",
+ 1: "ECDSA_KEY",
+ 2: "XTS_AES_256_KEY_1",
+ 3: "XTS_AES_256_KEY_2",
+ 4: "XTS_AES_128_KEY",
+ 5: "HMAC_DOWN_ALL",
+ 6: "HMAC_DOWN_JTAG",
+ 7: "HMAC_DOWN_DIGITAL_SIGNATURE",
+ 8: "HMAC_UP",
+ 9: "SECURE_BOOT_DIGEST0",
+ 10: "SECURE_BOOT_DIGEST1",
+ 11: "SECURE_BOOT_DIGEST2",
+ 12: "KM_INIT_KEY",
+ }
+
+ def get_pkg_version(self):
+ num_word = 2
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 26) & 0x07
+
+ def get_minor_chip_version(self):
+ num_word = 2
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
+
+ def get_major_chip_version(self):
+ num_word = 2
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 4) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C5",
+ }.get(self.get_pkg_version(), "unknown ESP32-C5")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_crystal_freq(self):
+ # The crystal detection algorithm of ESP32/ESP8266
+ # works for ESP32-C5 as well.
+ return ESPLoader.get_crystal_freq(self)
+
+ def get_crystal_freq_rom_expect(self):
+ return (
+ self.read_reg(self.PCR_SYSCLK_CONF_REG) & self.PCR_SYSCLK_XTAL_FREQ_V
+ ) >> self.PCR_SYSCLK_XTAL_FREQ_S
+
+ def hard_reset(self):
+ print("Hard resetting via RTS pin...")
+ HardReset(self._port, self.uses_usb_jtag_serial())()
+
+ def change_baud(self, baud):
+ if not self.IS_STUB:
+ crystal_freq_rom_expect = self.get_crystal_freq_rom_expect()
+ crystal_freq_detect = self.get_crystal_freq()
+ print(
+ f"ROM expects crystal freq: {crystal_freq_rom_expect} MHz, detected {crystal_freq_detect} MHz"
+ )
+ baud_rate = baud
+ # If detect the XTAL is 48MHz, but the ROM code expects it to be 40MHz
+ if crystal_freq_detect == 48 and crystal_freq_rom_expect == 40:
+ baud_rate = baud * 40 // 48
+ # If detect the XTAL is 40MHz, but the ROM code expects it to be 48MHz
+ elif crystal_freq_detect == 40 and crystal_freq_rom_expect == 48:
+ baud_rate = baud * 48 // 40
+ else:
+ ESPLoader.change_baud(self, baud_rate)
+ return
+
+ print(f"Changing baud rate to {baud_rate}")
+ self.command(self.ESP_CHANGE_BAUDRATE, struct.pack("> 26) & 0x07
+
+ def get_minor_chip_version(self):
+ num_word = 2
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
+
+ def get_major_chip_version(self):
+ num_word = 2
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 4) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C5 beta3 (QFN40)",
+ }.get(self.get_pkg_version(), "unknown ESP32-C5 beta3")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_crystal_freq(self):
+ # The crystal detection algorithm of ESP32/ESP8266
+ # works for ESP32-C5 beta3 as well.
+ return ESPLoader.get_crystal_freq(self)
+
+ def change_baud(self, baud):
+ rom_with_48M_XTAL = not self.IS_STUB and self.get_crystal_freq() == 48
+ if rom_with_48M_XTAL:
+ # The code is copied over from ESPLoader.change_baud().
+ # Probably this is just a temporary solution until the next chip revision.
+
+ # The ROM code thinks it uses a 40 MHz XTAL. Recompute the baud rate
+ # in order to trick the ROM code to set the correct baud rate for
+ # a 48 MHz XTAL.
+ false_rom_baud = baud * 40 // 48
+
+ print(f"Changing baud rate to {baud}")
+ self.command(
+ self.ESP_CHANGE_BAUDRATE, struct.pack("> 24) & 0x07
+
+ def get_minor_chip_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x0F
+
+ def get_major_chip_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 22) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C6 (QFN40)",
+ 1: "ESP32-C6FH4 (QFN32)",
+ }.get(self.get_pkg_version(), "unknown ESP32-C6")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["WiFi 6", "BT 5", "IEEE802.15.4"]
+
+ def get_crystal_freq(self):
+ # ESP32C6 XTAL is fixed to 40MHz
+ return 40
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-C6"
+ )
+
+ def read_mac(self, mac_type="BASE_MAC"):
+ """Read MAC from EFUSE region"""
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ base_mac = struct.pack(">II", mac1, mac0)[2:]
+ ext_mac = struct.pack(">H", (mac1 >> 16) & 0xFFFF)
+ eui64 = base_mac[0:3] + ext_mac + base_mac[3:6]
+ # BASE MAC: 60:55:f9:f7:2c:a2
+ # EUI64 MAC: 60:55:f9:ff:fe:f7:2c:a2
+ # EXT_MAC: ff:fe
+ macs = {
+ "BASE_MAC": tuple(base_mac),
+ "EUI64": tuple(eui64),
+ "MAC_EXT": tuple(ext_mac),
+ }
+ return macs.get(mac_type, None)
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-C6
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > self.EFUSE_MAX_KEY:
+ raise FatalError(
+ f"Valid key block numbers must be in range 0-{self.EFUSE_MAX_KEY}"
+ )
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see an AES-128 key
+ purposes = [
+ self.get_key_block_purpose(b) for b in range(self.EFUSE_MAX_KEY + 1)
+ ]
+
+ return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
+
+ def check_spi_connection(self, spi_connection):
+ if not set(spi_connection).issubset(set(range(0, 31))):
+ raise FatalError("SPI Pin numbers must be in the range 0-30.")
+ if any([v for v in spi_connection if v in [12, 13]]):
+ print(
+ "WARNING: GPIO pins 12 and 13 are used by USB-Serial/JTAG, "
+ "consider using other pins for SPI flash connection."
+ )
+
+
+class ESP32C6StubLoader(ESP32C6ROM):
+ """Access class for ESP32C6 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32C6ROM.STUB_CLASS = ESP32C6StubLoader
diff --git a/mixly/tools/python/esptool/targets/esp32c61.py b/mixly/tools/python/esptool/targets/esp32c61.py
new file mode 100644
index 00000000..2132bda3
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp32c61.py
@@ -0,0 +1,144 @@
+# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import struct
+from typing import Dict
+
+from .esp32c6 import ESP32C6ROM
+
+
+class ESP32C61ROM(ESP32C6ROM):
+ CHIP_NAME = "ESP32-C61"
+ IMAGE_CHIP_ID = 20
+
+ # Magic value for ESP32C61
+ CHIP_DETECT_MAGIC_VALUE = [0x33F0206F, 0x2421606F]
+
+ UART_DATE_REG_ADDR = 0x60000000 + 0x7C
+
+ EFUSE_BASE = 0x600B4800
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+ MAC_EFUSE_REG = EFUSE_BASE + 0x044
+
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 0
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 4
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY2_SHIFT = 8
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY3_SHIFT = 12
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY4_SHIFT = 16
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY5_SHIFT = 20
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x030
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 23
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x034
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 26
+
+ FLASH_FREQUENCY = {
+ "80m": 0xF,
+ "40m": 0x0,
+ "20m": 0x2,
+ }
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x41800000, 0x42000000, "DROM"],
+ [0x40800000, 0x40860000, "DRAM"],
+ [0x40800000, 0x40860000, "BYTE_ACCESSIBLE"],
+ [0x4004AC00, 0x40050000, "DROM_MASK"],
+ [0x40000000, 0x4004AC00, "IROM_MASK"],
+ [0x41000000, 0x41800000, "IROM"],
+ [0x40800000, 0x40860000, "IRAM"],
+ [0x50000000, 0x50004000, "RTC_IRAM"],
+ [0x50000000, 0x50004000, "RTC_DRAM"],
+ [0x600FE000, 0x60100000, "MEM_INTERNAL2"],
+ ]
+
+ UF2_FAMILY_ID = 0x77D850C4
+
+ EFUSE_MAX_KEY = 5
+ KEY_PURPOSES: Dict[int, str] = {
+ 0: "USER/EMPTY",
+ 1: "ECDSA_KEY",
+ 2: "XTS_AES_256_KEY_1",
+ 3: "XTS_AES_256_KEY_2",
+ 4: "XTS_AES_128_KEY",
+ 5: "HMAC_DOWN_ALL",
+ 6: "HMAC_DOWN_JTAG",
+ 7: "HMAC_DOWN_DIGITAL_SIGNATURE",
+ 8: "HMAC_UP",
+ 9: "SECURE_BOOT_DIGEST0",
+ 10: "SECURE_BOOT_DIGEST1",
+ 11: "SECURE_BOOT_DIGEST2",
+ 12: "KM_INIT_KEY",
+ 13: "XTS_AES_256_KEY_1_PSRAM",
+ 14: "XTS_AES_256_KEY_2_PSRAM",
+ 15: "XTS_AES_128_KEY_PSRAM",
+ }
+
+ def get_pkg_version(self):
+ num_word = 2
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 26) & 0x07
+
+ def get_minor_chip_version(self):
+ num_word = 2
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
+
+ def get_major_chip_version(self):
+ num_word = 2
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 4) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C61",
+ }.get(self.get_pkg_version(), "unknown ESP32-C61")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["WiFi 6", "BT 5"]
+
+ def read_mac(self, mac_type="BASE_MAC"):
+ """Read MAC from EFUSE region"""
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ base_mac = struct.pack(">II", mac1, mac0)[2:]
+ # BASE MAC: 60:55:f9:f7:2c:a2
+ macs = {
+ "BASE_MAC": tuple(base_mac),
+ }
+ return macs.get(mac_type, None)
+
+
+class ESP32C61StubLoader(ESP32C61ROM):
+ """Access class for ESP32C61 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32C61ROM.STUB_CLASS = ESP32C61StubLoader
diff --git a/mixly/tools/python/esptool/targets/esp32c6beta.py b/mixly/tools/python/esptool/targets/esp32c6beta.py
new file mode 100644
index 00000000..b6e100bb
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp32c6beta.py
@@ -0,0 +1,27 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from .esp32c3 import ESP32C3ROM
+
+
+class ESP32C6BETAROM(ESP32C3ROM):
+ CHIP_NAME = "ESP32-C6(beta)"
+ IMAGE_CHIP_ID = 7
+
+ CHIP_DETECT_MAGIC_VALUE = [0x0DA1806F]
+
+ UART_DATE_REG_ADDR = 0x00000500
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C6 (QFN40)",
+ 1: "ESP32-C6FH4 (QFN32)",
+ }.get(self.get_pkg_version(), "unknown ESP32-C6")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def _post_connect(self):
+ pass
diff --git a/mixly/tools/python/esptool/targets/esp32h2.py b/mixly/tools/python/esptool/targets/esp32h2.py
new file mode 100644
index 00000000..da7d34a5
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp32h2.py
@@ -0,0 +1,109 @@
+# SPDX-FileCopyrightText: 2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from typing import Dict
+
+from .esp32c6 import ESP32C6ROM
+from ..util import FatalError
+
+
+class ESP32H2ROM(ESP32C6ROM):
+ CHIP_NAME = "ESP32-H2"
+ IMAGE_CHIP_ID = 16
+
+ # Magic value for ESP32H2
+ CHIP_DETECT_MAGIC_VALUE = [0xD7B73E80]
+
+ DR_REG_LP_WDT_BASE = 0x600B1C00
+ RTC_CNTL_WDTCONFIG0_REG = DR_REG_LP_WDT_BASE + 0x0 # LP_WDT_RWDT_CONFIG0_REG
+ RTC_CNTL_WDTWPROTECT_REG = DR_REG_LP_WDT_BASE + 0x001C # LP_WDT_RWDT_WPROTECT_REG
+
+ RTC_CNTL_SWD_CONF_REG = DR_REG_LP_WDT_BASE + 0x0020 # LP_WDT_SWD_CONFIG_REG
+ RTC_CNTL_SWD_AUTO_FEED_EN = 1 << 18
+ RTC_CNTL_SWD_WPROTECT_REG = DR_REG_LP_WDT_BASE + 0x0024 # LP_WDT_SWD_WPROTECT_REG
+ RTC_CNTL_SWD_WKEY = 0x50D83AA1 # LP_WDT_SWD_WKEY, same as WDT key in this case
+
+ FLASH_FREQUENCY = {
+ "48m": 0xF,
+ "24m": 0x0,
+ "16m": 0x1,
+ "12m": 0x2,
+ }
+
+ UF2_FAMILY_ID = 0x332726F6
+
+ EFUSE_MAX_KEY = 5
+ KEY_PURPOSES: Dict[int, str] = {
+ 0: "USER/EMPTY",
+ 1: "ECDSA_KEY",
+ 2: "XTS_AES_256_KEY_1",
+ 3: "XTS_AES_256_KEY_2",
+ 4: "XTS_AES_128_KEY",
+ 5: "HMAC_DOWN_ALL",
+ 6: "HMAC_DOWN_JTAG",
+ 7: "HMAC_DOWN_DIGITAL_SIGNATURE",
+ 8: "HMAC_UP",
+ 9: "SECURE_BOOT_DIGEST0",
+ 10: "SECURE_BOOT_DIGEST1",
+ 11: "SECURE_BOOT_DIGEST2",
+ }
+
+ def get_pkg_version(self):
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x07
+
+ def get_minor_chip_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x07
+
+ def get_major_chip_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-H2",
+ }.get(self.get_pkg_version(), "unknown ESP32-H2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["BLE", "IEEE802.15.4"]
+
+ def get_crystal_freq(self):
+ # ESP32H2 XTAL is fixed to 32MHz
+ return 32
+
+ def check_spi_connection(self, spi_connection):
+ if not set(spi_connection).issubset(set(range(0, 28))):
+ raise FatalError("SPI Pin numbers must be in the range 0-27.")
+ if any([v for v in spi_connection if v in [26, 27]]):
+ print(
+ "WARNING: GPIO pins 26 and 27 are used by USB-Serial/JTAG, "
+ "consider using other pins for SPI flash connection."
+ )
+
+
+class ESP32H2StubLoader(ESP32H2ROM):
+ """Access class for ESP32H2 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32H2ROM.STUB_CLASS = ESP32H2StubLoader
diff --git a/mixly/tools/python/esptool/targets/esp32h2beta1.py b/mixly/tools/python/esptool/targets/esp32h2beta1.py
new file mode 100644
index 00000000..999a16e0
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp32h2beta1.py
@@ -0,0 +1,186 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import struct
+from typing import Dict
+
+from .esp32c3 import ESP32C3ROM
+from ..util import FatalError, NotImplementedInROMError
+
+from typing import List
+
+
+class ESP32H2BETA1ROM(ESP32C3ROM):
+ CHIP_NAME = "ESP32-H2(beta1)"
+ IMAGE_CHIP_ID = 10
+
+ IROM_MAP_START = 0x42000000
+ IROM_MAP_END = 0x42800000
+ DROM_MAP_START = 0x3C000000
+ DROM_MAP_END = 0x3C800000
+
+ SPI_REG_BASE = 0x60002000
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ BOOTLOADER_FLASH_OFFSET = 0x0
+
+ CHIP_DETECT_MAGIC_VALUE = [0xCA26CC22]
+
+ UART_DATE_REG_ADDR = 0x60000000 + 0x7C
+
+ EFUSE_BASE = 0x6001A000
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+ MAC_EFUSE_REG = EFUSE_BASE + 0x044
+
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ MEMORY_MAP: List = []
+
+ FLASH_FREQUENCY = {
+ "48m": 0xF,
+ "24m": 0x0,
+ "16m": 0x1,
+ "12m": 0x2,
+ }
+
+ EFUSE_MAX_KEY = 5
+ KEY_PURPOSES: Dict[int, str] = {
+ 0: "USER/EMPTY",
+ 1: "ECDSA_KEY",
+ 2: "RESERVED",
+ 4: "XTS_AES_128_KEY",
+ 5: "HMAC_DOWN_ALL",
+ 6: "HMAC_DOWN_JTAG",
+ 7: "HMAC_DOWN_DIGITAL_SIGNATURE",
+ 8: "HMAC_UP",
+ 9: "SECURE_BOOT_DIGEST0",
+ 10: "SECURE_BOOT_DIGEST1",
+ 11: "SECURE_BOOT_DIGEST2",
+ }
+
+ def get_pkg_version(self):
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x07
+
+ def get_minor_chip_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x07
+
+ def get_major_chip_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-H2",
+ }.get(self.get_pkg_version(), "unknown ESP32-H2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["BLE", "IEEE802.15.4"]
+
+ def get_crystal_freq(self):
+ return 32
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-H2"
+ )
+
+ def read_mac(self, mac_type="BASE_MAC"):
+ """Read MAC from EFUSE region"""
+ if mac_type != "BASE_MAC":
+ return None
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-H2
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > self.EFUSE_MAX_KEY:
+ raise FatalError(
+ f"Valid key block numbers must be in range 0-{self.EFUSE_MAX_KEY}"
+ )
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see an AES-128 key
+ purposes = [
+ self.get_key_block_purpose(b) for b in range(self.EFUSE_MAX_KEY + 1)
+ ]
+
+ return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
+
+ def _post_connect(self):
+ pass
+
+
+class ESP32H2BETA1StubLoader(ESP32H2BETA1ROM):
+ """Access class for ESP32H2BETA1 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32H2BETA1ROM.STUB_CLASS = ESP32H2BETA1StubLoader
diff --git a/mixly/tools/python/esptool/targets/esp32h2beta2.py b/mixly/tools/python/esptool/targets/esp32h2beta2.py
new file mode 100644
index 00000000..6fa8f587
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp32h2beta2.py
@@ -0,0 +1,43 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from .esp32h2beta1 import ESP32H2BETA1ROM
+
+
+class ESP32H2BETA2ROM(ESP32H2BETA1ROM):
+ CHIP_NAME = "ESP32-H2(beta2)"
+ IMAGE_CHIP_ID = 14
+
+ CHIP_DETECT_MAGIC_VALUE = [0x6881B06F]
+
+ def get_chip_description(self):
+ chip_name = {
+ 1: "ESP32-H2(beta2)",
+ }.get(self.get_pkg_version(), "unknown ESP32-H2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+
+class ESP32H2BETA2StubLoader(ESP32H2BETA2ROM):
+ """Access class for ESP32H2BETA2 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32H2BETA2ROM.STUB_CLASS = ESP32H2BETA2StubLoader
diff --git a/mixly/tools/python/esptool/targets/esp32p4.py b/mixly/tools/python/esptool/targets/esp32p4.py
new file mode 100644
index 00000000..2b0a0649
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp32p4.py
@@ -0,0 +1,228 @@
+# SPDX-FileCopyrightText: 2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import struct
+from typing import Dict
+
+from .esp32 import ESP32ROM
+from ..loader import ESPLoader
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP32P4ROM(ESP32ROM):
+ CHIP_NAME = "ESP32-P4"
+ IMAGE_CHIP_ID = 18
+
+ IROM_MAP_START = 0x40000000
+ IROM_MAP_END = 0x4C000000
+ DROM_MAP_START = 0x40000000
+ DROM_MAP_END = 0x4C000000
+
+ BOOTLOADER_FLASH_OFFSET = 0x2000 # First 2 sectors are reserved for FE purposes
+
+ CHIP_DETECT_MAGIC_VALUE = [0x0, 0x0ADDBAD0]
+
+ UART_DATE_REG_ADDR = 0x500CA000 + 0x8C
+
+ EFUSE_BASE = 0x5012D000
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+ MAC_EFUSE_REG = EFUSE_BASE + 0x044
+
+ SPI_REG_BASE = 0x5008D000 # SPIMEM1
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ SPI_ADDR_REG_MSB = False
+
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ PURPOSE_VAL_XTS_AES256_KEY_1 = 2
+ PURPOSE_VAL_XTS_AES256_KEY_2 = 3
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x40000000, 0x4C000000, "DROM"],
+ [0x4FF00000, 0x4FFA0000, "DRAM"],
+ [0x4FF00000, 0x4FFA0000, "BYTE_ACCESSIBLE"],
+ [0x4FC00000, 0x4FC20000, "DROM_MASK"],
+ [0x4FC00000, 0x4FC20000, "IROM_MASK"],
+ [0x40000000, 0x4C000000, "IROM"],
+ [0x4FF00000, 0x4FFA0000, "IRAM"],
+ [0x50108000, 0x50110000, "RTC_IRAM"],
+ [0x50108000, 0x50110000, "RTC_DRAM"],
+ [0x600FE000, 0x60100000, "MEM_INTERNAL2"],
+ ]
+
+ UF2_FAMILY_ID = 0x3D308E94
+
+ EFUSE_MAX_KEY = 5
+ KEY_PURPOSES: Dict[int, str] = {
+ 0: "USER/EMPTY",
+ 1: "ECDSA_KEY",
+ 2: "XTS_AES_256_KEY_1",
+ 3: "XTS_AES_256_KEY_2",
+ 4: "XTS_AES_128_KEY",
+ 5: "HMAC_DOWN_ALL",
+ 6: "HMAC_DOWN_JTAG",
+ 7: "HMAC_DOWN_DIGITAL_SIGNATURE",
+ 8: "HMAC_UP",
+ 9: "SECURE_BOOT_DIGEST0",
+ 10: "SECURE_BOOT_DIGEST1",
+ 11: "SECURE_BOOT_DIGEST2",
+ 12: "KM_INIT_KEY",
+ }
+
+ def get_pkg_version(self):
+ num_word = 2
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 20) & 0x07
+
+ def get_minor_chip_version(self):
+ num_word = 2
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
+
+ def get_major_chip_version(self):
+ num_word = 2
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 4) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-P4",
+ }.get(self.get_pkg_version(), "unknown ESP32-P4")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["High-Performance MCU"]
+
+ def get_crystal_freq(self):
+ # ESP32P4 XTAL is fixed to 40MHz
+ return 40
+
+ def get_flash_voltage(self):
+ pass # not supported on ESP32-P4
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-P4"
+ )
+
+ def read_mac(self, mac_type="BASE_MAC"):
+ """Read MAC from EFUSE region"""
+ if mac_type != "BASE_MAC":
+ return None
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-P4
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > self.EFUSE_MAX_KEY:
+ raise FatalError(
+ f"Valid key block numbers must be in range 0-{self.EFUSE_MAX_KEY}"
+ )
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see either an AES-128 key or two AES-256 keys
+ purposes = [
+ self.get_key_block_purpose(b) for b in range(self.EFUSE_MAX_KEY + 1)
+ ]
+
+ if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes):
+ return True
+
+ return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) and any(
+ p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes
+ )
+
+ def change_baud(self, baud):
+ ESPLoader.change_baud(self, baud)
+
+ def _post_connect(self):
+ pass
+ # TODO: Disable watchdogs when USB modes are supported in the stub
+ # if not self.sync_stub_detected: # Don't run if stub is reused
+ # self.disable_watchdogs()
+
+ def check_spi_connection(self, spi_connection):
+ if not set(spi_connection).issubset(set(range(0, 55))):
+ raise FatalError("SPI Pin numbers must be in the range 0-54.")
+ if any([v for v in spi_connection if v in [24, 25]]):
+ print(
+ "WARNING: GPIO pins 24 and 25 are used by USB-Serial/JTAG, "
+ "consider using other pins for SPI flash connection."
+ )
+
+
+class ESP32P4StubLoader(ESP32P4ROM):
+ """Access class for ESP32P4 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32P4ROM.STUB_CLASS = ESP32P4StubLoader
diff --git a/mixly/tools/python/esptool/targets/esp32s2.py b/mixly/tools/python/esptool/targets/esp32s2.py
new file mode 100644
index 00000000..e16f532f
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp32s2.py
@@ -0,0 +1,352 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import struct
+from typing import Dict
+
+from .esp32 import ESP32ROM
+from ..loader import ESPLoader
+from ..reset import HardReset
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP32S2ROM(ESP32ROM):
+ CHIP_NAME = "ESP32-S2"
+ IMAGE_CHIP_ID = 2
+
+ IROM_MAP_START = 0x40080000
+ IROM_MAP_END = 0x40B80000
+ DROM_MAP_START = 0x3F000000
+ DROM_MAP_END = 0x3F3F0000
+
+ CHIP_DETECT_MAGIC_VALUE = [0x000007C6]
+
+ SPI_REG_BASE = 0x3F402000
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ SPI_ADDR_REG_MSB = False
+
+ MAC_EFUSE_REG = 0x3F41A044 # ESP32-S2 has special block for MAC efuses
+
+ UART_CLKDIV_REG = 0x3F400014
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ # todo: use espefuse APIs to get this info
+ EFUSE_BASE = 0x3F41A000
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+ EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x05C
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 19
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ EFUSE_RD_REPEAT_DATA3_REG = EFUSE_BASE + 0x3C
+ EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK = 1 << 9
+
+ PURPOSE_VAL_XTS_AES256_KEY_1 = 2
+ PURPOSE_VAL_XTS_AES256_KEY_2 = 3
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ UARTDEV_BUF_NO = 0x3FFFFD14 # Variable in ROM .bss which indicates the port in use
+ UARTDEV_BUF_NO_USB_OTG = 2 # Value of the above indicating that USB-OTG is in use
+
+ USB_RAM_BLOCK = 0x800 # Max block size USB-OTG is used
+
+ GPIO_STRAP_REG = 0x3F404038
+ GPIO_STRAP_SPI_BOOT_MASK = 0x8 # Not download mode
+ GPIO_STRAP_VDDSPI_MASK = 1 << 4
+ RTC_CNTL_OPTION1_REG = 0x3F408128
+ RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1 # Is download mode forced over USB?
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x3F000000, 0x3FF80000, "DROM"],
+ [0x3F500000, 0x3FF80000, "EXTRAM_DATA"],
+ [0x3FF9E000, 0x3FFA0000, "RTC_DRAM"],
+ [0x3FF9E000, 0x40000000, "BYTE_ACCESSIBLE"],
+ [0x3FF9E000, 0x40072000, "MEM_INTERNAL"],
+ [0x3FFB0000, 0x40000000, "DRAM"],
+ [0x40000000, 0x4001A100, "IROM_MASK"],
+ [0x40020000, 0x40070000, "IRAM"],
+ [0x40070000, 0x40072000, "RTC_IRAM"],
+ [0x40080000, 0x40800000, "IROM"],
+ [0x50000000, 0x50002000, "RTC_DATA"],
+ ]
+
+ EFUSE_VDD_SPI_REG = EFUSE_BASE + 0x34
+ VDD_SPI_XPD = 1 << 4
+ VDD_SPI_TIEH = 1 << 5
+ VDD_SPI_FORCE = 1 << 6
+
+ UF2_FAMILY_ID = 0xBFDD4EEE
+
+ EFUSE_MAX_KEY = 5
+ KEY_PURPOSES: Dict[int, str] = {
+ 0: "USER/EMPTY",
+ 1: "RESERVED",
+ 2: "XTS_AES_256_KEY_1",
+ 3: "XTS_AES_256_KEY_2",
+ 4: "XTS_AES_128_KEY",
+ 5: "HMAC_DOWN_ALL",
+ 6: "HMAC_DOWN_JTAG",
+ 7: "HMAC_DOWN_DIGITAL_SIGNATURE",
+ 8: "HMAC_UP",
+ 9: "SECURE_BOOT_DIGEST0",
+ 10: "SECURE_BOOT_DIGEST1",
+ 11: "SECURE_BOOT_DIGEST2",
+ }
+
+ def get_pkg_version(self):
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
+
+ def get_minor_chip_version(self):
+ hi_num_word = 3
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 20) & 0x01
+ low_num_word = 4
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 4) & 0x07
+ return (hi << 3) + low
+
+ def get_major_chip_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x03
+
+ def get_flash_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x0F
+
+ def get_flash_cap(self):
+ return self.get_flash_version()
+
+ def get_psram_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 28) & 0x0F
+
+ def get_psram_cap(self):
+ return self.get_psram_version()
+
+ def get_block2_version(self):
+ # BLK_VERSION_MINOR
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 4) & 0x07
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-S2",
+ 1: "ESP32-S2FH2",
+ 2: "ESP32-S2FH4",
+ 102: "ESP32-S2FNR2",
+ 100: "ESP32-S2R2",
+ }.get(
+ self.get_flash_cap() + self.get_psram_cap() * 100,
+ "unknown ESP32-S2",
+ )
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ features = ["WiFi"]
+
+ if self.secure_download_mode:
+ features += ["Secure Download Mode Enabled"]
+
+ flash_version = {
+ 0: "No Embedded Flash",
+ 1: "Embedded Flash 2MB",
+ 2: "Embedded Flash 4MB",
+ }.get(self.get_flash_cap(), "Unknown Embedded Flash")
+ features += [flash_version]
+
+ psram_version = {
+ 0: "No Embedded PSRAM",
+ 1: "Embedded PSRAM 2MB",
+ 2: "Embedded PSRAM 4MB",
+ }.get(self.get_psram_cap(), "Unknown Embedded PSRAM")
+ features += [psram_version]
+
+ block2_version = {
+ 0: "No calibration in BLK2 of efuse",
+ 1: "ADC and temperature sensor calibration in BLK2 of efuse V1",
+ 2: "ADC and temperature sensor calibration in BLK2 of efuse V2",
+ }.get(self.get_block2_version(), "Unknown Calibration in BLK2")
+ features += [block2_version]
+
+ return features
+
+ def get_crystal_freq(self):
+ # ESP32-S2 XTAL is fixed to 40MHz
+ return 40
+
+ def _get_rtc_cntl_flash_voltage(self):
+ return None # not supported on ESP32-S2
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-S2"
+ )
+
+ def read_mac(self, mac_type="BASE_MAC"):
+ """Read MAC from EFUSE region"""
+ if mac_type != "BASE_MAC":
+ return None
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def flash_type(self):
+ return (
+ 1
+ if self.read_reg(self.EFUSE_RD_REPEAT_DATA3_REG)
+ & self.EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK
+ else 0
+ )
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-S2
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > self.EFUSE_MAX_KEY:
+ raise FatalError(
+ f"Valid key block numbers must be in range 0-{self.EFUSE_MAX_KEY}"
+ )
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see either an AES-128 key or two AES-256 keys
+ purposes = [
+ self.get_key_block_purpose(b) for b in range(self.EFUSE_MAX_KEY + 1)
+ ]
+
+ if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes):
+ return True
+
+ return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) and any(
+ p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes
+ )
+
+ def uses_usb_otg(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-OTG console is being used
+ """
+ if self.secure_download_mode:
+ return False # can't detect native USB in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_OTG
+
+ def _post_connect(self):
+ if self.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+
+ def _check_if_can_reset(self):
+ """
+ Check the strapping register to see if we can reset out of download mode.
+ """
+ if os.getenv("ESPTOOL_TESTING") is not None:
+ print("ESPTOOL_TESTING is set, ignoring strapping mode check")
+ # Esptool tests over USB-OTG run with GPIO0 strapped low,
+ # don't complain in this case.
+ return
+ strap_reg = self.read_reg(self.GPIO_STRAP_REG)
+ force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG)
+ if (
+ strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0
+ and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0
+ ):
+ raise SystemExit(
+ f"Error: {self.get_chip_description()} chip was placed into download "
+ "mode using GPIO0.\nesptool.py can not exit the download mode over "
+ "USB. To run the app, reset the chip manually.\n"
+ "To suppress this note, set --after option to 'no_reset'."
+ )
+
+ def hard_reset(self):
+ uses_usb_otg = self.uses_usb_otg()
+ if uses_usb_otg:
+ self._check_if_can_reset()
+
+ print("Hard resetting via RTS pin...")
+ HardReset(self._port, uses_usb_otg)()
+
+ def change_baud(self, baud):
+ ESPLoader.change_baud(self, baud)
+
+ def check_spi_connection(self, spi_connection):
+ if not set(spi_connection).issubset(set(range(0, 22)) | set(range(26, 47))):
+ raise FatalError("SPI Pin numbers must be in the range 0-21, or 26-46.")
+ if any([v for v in spi_connection if v in [19, 20]]):
+ print(
+ "WARNING: GPIO pins 19 and 20 are used by USB-OTG, "
+ "consider using other pins for SPI flash connection."
+ )
+
+
+class ESP32S2StubLoader(ESP32S2ROM):
+ """Access class for ESP32-S2 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+ if rom_loader.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+ self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK
+
+
+ESP32S2ROM.STUB_CLASS = ESP32S2StubLoader
diff --git a/mixly/tools/python/esptool/targets/esp32s3.py b/mixly/tools/python/esptool/targets/esp32s3.py
new file mode 100644
index 00000000..fb0f23a2
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp32s3.py
@@ -0,0 +1,426 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import struct
+from typing import Dict
+
+from .esp32 import ESP32ROM
+from ..loader import ESPLoader
+from ..reset import HardReset
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP32S3ROM(ESP32ROM):
+ CHIP_NAME = "ESP32-S3"
+
+ IMAGE_CHIP_ID = 9
+
+ CHIP_DETECT_MAGIC_VALUE = [0x9]
+
+ IROM_MAP_START = 0x42000000
+ IROM_MAP_END = 0x44000000
+ DROM_MAP_START = 0x3C000000
+ DROM_MAP_END = 0x3E000000
+
+ UART_DATE_REG_ADDR = 0x60000080
+
+ SPI_REG_BASE = 0x60002000
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ SPI_ADDR_REG_MSB = False
+
+ BOOTLOADER_FLASH_OFFSET = 0x0
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ # todo: use espefuse APIs to get this info
+ EFUSE_BASE = 0x60007000 # BLOCK0 read base address
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x44
+ EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x5C
+ MAC_EFUSE_REG = EFUSE_BASE + 0x044
+
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ EFUSE_RD_REPEAT_DATA3_REG = EFUSE_BASE + 0x3C
+ EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK = 1 << 9
+
+ PURPOSE_VAL_XTS_AES256_KEY_1 = 2
+ PURPOSE_VAL_XTS_AES256_KEY_2 = 3
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ UARTDEV_BUF_NO = 0x3FCEF14C # Variable in ROM .bss which indicates the port in use
+ UARTDEV_BUF_NO_USB_OTG = 3 # The above var when USB-OTG is used
+ UARTDEV_BUF_NO_USB_JTAG_SERIAL = 4 # The above var when USB-JTAG/Serial is used
+
+ RTCCNTL_BASE_REG = 0x60008000
+ RTC_CNTL_SWD_CONF_REG = RTCCNTL_BASE_REG + 0x00B4
+ RTC_CNTL_SWD_AUTO_FEED_EN = 1 << 31
+ RTC_CNTL_SWD_WPROTECT_REG = RTCCNTL_BASE_REG + 0x00B8
+ RTC_CNTL_SWD_WKEY = 0x8F1D312A
+
+ RTC_CNTL_WDTCONFIG0_REG = RTCCNTL_BASE_REG + 0x0098
+ RTC_CNTL_WDTWPROTECT_REG = RTCCNTL_BASE_REG + 0x00B0
+ RTC_CNTL_WDT_WKEY = 0x50D83AA1
+
+ USB_RAM_BLOCK = 0x800 # Max block size USB-OTG is used
+
+ GPIO_STRAP_REG = 0x60004038
+ GPIO_STRAP_SPI_BOOT_MASK = 0x8 # Not download mode
+ GPIO_STRAP_VDDSPI_MASK = 1 << 4
+ RTC_CNTL_OPTION1_REG = 0x6000812C
+ RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1 # Is download mode forced over USB?
+
+ UART_CLKDIV_REG = 0x60000014
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x3C000000, 0x3D000000, "DROM"],
+ [0x3D000000, 0x3E000000, "EXTRAM_DATA"],
+ [0x600FE000, 0x60100000, "RTC_DRAM"],
+ [0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"],
+ [0x3FC88000, 0x403E2000, "MEM_INTERNAL"],
+ [0x3FC88000, 0x3FD00000, "DRAM"],
+ [0x40000000, 0x4001A100, "IROM_MASK"],
+ [0x40370000, 0x403E0000, "IRAM"],
+ [0x600FE000, 0x60100000, "RTC_IRAM"],
+ [0x42000000, 0x42800000, "IROM"],
+ [0x50000000, 0x50002000, "RTC_DATA"],
+ ]
+
+ EFUSE_VDD_SPI_REG = EFUSE_BASE + 0x34
+ VDD_SPI_XPD = 1 << 4
+ VDD_SPI_TIEH = 1 << 5
+ VDD_SPI_FORCE = 1 << 6
+
+ UF2_FAMILY_ID = 0xC47E5767
+
+ EFUSE_MAX_KEY = 5
+ KEY_PURPOSES: Dict[int, str] = {
+ 0: "USER/EMPTY",
+ 1: "RESERVED",
+ 2: "XTS_AES_256_KEY_1",
+ 3: "XTS_AES_256_KEY_2",
+ 4: "XTS_AES_128_KEY",
+ 5: "HMAC_DOWN_ALL",
+ 6: "HMAC_DOWN_JTAG",
+ 7: "HMAC_DOWN_DIGITAL_SIGNATURE",
+ 8: "HMAC_UP",
+ 9: "SECURE_BOOT_DIGEST0",
+ 10: "SECURE_BOOT_DIGEST1",
+ 11: "SECURE_BOOT_DIGEST2",
+ }
+
+ def get_pkg_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
+
+ def is_eco0(self, minor_raw):
+ # Workaround: The major version field was allocated to other purposes
+ # when block version is v1.1.
+ # Luckily only chip v0.0 have this kind of block version and efuse usage.
+ return (
+ (minor_raw & 0x7) == 0
+ and self.get_blk_version_major() == 1
+ and self.get_blk_version_minor() == 1
+ )
+
+ def get_minor_chip_version(self):
+ minor_raw = self.get_raw_minor_chip_version()
+ if self.is_eco0(minor_raw):
+ return 0
+ return minor_raw
+
+ def get_raw_minor_chip_version(self):
+ hi_num_word = 5
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+ low_num_word = 3
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+ return (hi << 3) + low
+
+ def get_blk_version_major(self):
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 0) & 0x03
+
+ def get_blk_version_minor(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x07
+
+ def get_major_chip_version(self):
+ minor_raw = self.get_raw_minor_chip_version()
+ if self.is_eco0(minor_raw):
+ return 0
+ return self.get_raw_major_chip_version()
+
+ def get_raw_major_chip_version(self):
+ num_word = 5
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
+
+ def get_chip_description(self):
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ pkg_version = self.get_pkg_version()
+
+ chip_name = {
+ 0: "ESP32-S3 (QFN56)",
+ 1: "ESP32-S3-PICO-1 (LGA56)",
+ }.get(pkg_version, "unknown ESP32-S3")
+
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_flash_cap(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 27) & 0x07
+
+ def get_flash_vendor(self):
+ num_word = 4
+ vendor_id = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x07
+ return {1: "XMC", 2: "GD", 3: "FM", 4: "TT", 5: "BY"}.get(vendor_id, "")
+
+ def get_psram_cap(self):
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 3) & 0x03
+
+ def get_psram_vendor(self):
+ num_word = 4
+ vendor_id = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 7) & 0x03
+ return {1: "AP_3v3", 2: "AP_1v8"}.get(vendor_id, "")
+
+ def get_chip_features(self):
+ features = ["WiFi", "BLE"]
+
+ flash = {
+ 0: None,
+ 1: "Embedded Flash 8MB",
+ 2: "Embedded Flash 4MB",
+ }.get(self.get_flash_cap(), "Unknown Embedded Flash")
+ if flash is not None:
+ features += [flash + f" ({self.get_flash_vendor()})"]
+
+ psram = {
+ 0: None,
+ 1: "Embedded PSRAM 8MB",
+ 2: "Embedded PSRAM 2MB",
+ }.get(self.get_psram_cap(), "Unknown Embedded PSRAM")
+ if psram is not None:
+ features += [psram + f" ({self.get_psram_vendor()})"]
+
+ return features
+
+ def get_crystal_freq(self):
+ # ESP32S3 XTAL is fixed to 40MHz
+ return 40
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-S3
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > self.EFUSE_MAX_KEY:
+ raise FatalError(
+ f"Valid key block numbers must be in range 0-{self.EFUSE_MAX_KEY}"
+ )
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see either an AES-128 key or two AES-256 keys
+ purposes = [
+ self.get_key_block_purpose(b) for b in range(self.EFUSE_MAX_KEY + 1)
+ ]
+
+ if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes):
+ return True
+
+ return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) and any(
+ p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes
+ )
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def _get_rtc_cntl_flash_voltage(self):
+ return None # not supported on ESP32-S3
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-S3"
+ )
+
+ def read_mac(self, mac_type="BASE_MAC"):
+ """Read MAC from EFUSE region"""
+ if mac_type != "BASE_MAC":
+ return None
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def flash_type(self):
+ return (
+ 1
+ if self.read_reg(self.EFUSE_RD_REPEAT_DATA3_REG)
+ & self.EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK
+ else 0
+ )
+
+ def uses_usb_otg(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-OTG console is being used
+ """
+ if self.secure_download_mode:
+ return False # can't detect native USB in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_OTG
+
+ def uses_usb_jtag_serial(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-JTAG/Serial is being used
+ """
+ if self.secure_download_mode:
+ return False # can't detect USB-JTAG/Serial in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_JTAG_SERIAL
+
+ def disable_watchdogs(self):
+ # When USB-JTAG/Serial is used, the RTC WDT and SWD watchdog are not reset
+ # and can then reset the board during flashing. Disable them.
+ if self.uses_usb_jtag_serial():
+ # Disable RTC WDT
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY)
+ self.write_reg(self.RTC_CNTL_WDTCONFIG0_REG, 0)
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0)
+
+ # Automatically feed SWD
+ self.write_reg(self.RTC_CNTL_SWD_WPROTECT_REG, self.RTC_CNTL_SWD_WKEY)
+ self.write_reg(
+ self.RTC_CNTL_SWD_CONF_REG,
+ self.read_reg(self.RTC_CNTL_SWD_CONF_REG)
+ | self.RTC_CNTL_SWD_AUTO_FEED_EN,
+ )
+ self.write_reg(self.RTC_CNTL_SWD_WPROTECT_REG, 0)
+
+ def _post_connect(self):
+ if self.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+ if not self.sync_stub_detected: # Don't run if stub is reused
+ self.disable_watchdogs()
+
+ def _check_if_can_reset(self):
+ """
+ Check the strapping register to see if we can reset out of download mode.
+ """
+ if os.getenv("ESPTOOL_TESTING") is not None:
+ print("ESPTOOL_TESTING is set, ignoring strapping mode check")
+ # Esptool tests over USB-OTG run with GPIO0 strapped low,
+ # don't complain in this case.
+ return
+ strap_reg = self.read_reg(self.GPIO_STRAP_REG)
+ force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG)
+ if (
+ strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0
+ and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0
+ ):
+ raise SystemExit(
+ f"Error: {self.get_chip_description()} chip was placed into download "
+ "mode using GPIO0.\nesptool.py can not exit the download mode over "
+ "USB. To run the app, reset the chip manually.\n"
+ "To suppress this note, set --after option to 'no_reset'."
+ )
+
+ def hard_reset(self):
+ uses_usb_otg = self.uses_usb_otg()
+ if uses_usb_otg:
+ self._check_if_can_reset()
+
+ try:
+ # Clear force download boot mode to avoid the chip being stuck in download mode after reset
+ # workaround for issue: https://github.com/espressif/arduino-esp32/issues/6762
+ self.write_reg(
+ self.RTC_CNTL_OPTION1_REG, 0, self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK
+ )
+ except Exception:
+ # Skip if response was not valid and proceed to reset; e.g. when monitoring while resetting
+ pass
+
+ print("Hard resetting via RTS pin...")
+ HardReset(self._port, uses_usb_otg)()
+
+ def change_baud(self, baud):
+ ESPLoader.change_baud(self, baud)
+
+ def check_spi_connection(self, spi_connection):
+ if not set(spi_connection).issubset(set(range(0, 22)) | set(range(26, 49))):
+ raise FatalError("SPI Pin numbers must be in the range 0-21, or 26-48.")
+ if spi_connection[3] > 46: # hd_gpio_num must be <= SPI_GPIO_NUM_LIMIT (46)
+ raise FatalError("SPI HD Pin number must be <= 46.")
+ if any([v for v in spi_connection if v in [19, 20]]):
+ print(
+ "WARNING: GPIO pins 19 and 20 are used by USB-Serial/JTAG and USB-OTG, "
+ "consider using other pins for SPI flash connection."
+ )
+
+
+class ESP32S3StubLoader(ESP32S3ROM):
+ """Access class for ESP32S3 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+ if rom_loader.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+ self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK
+
+
+ESP32S3ROM.STUB_CLASS = ESP32S3StubLoader
diff --git a/mixly/tools/python/esptool/targets/esp32s3beta2.py b/mixly/tools/python/esptool/targets/esp32s3beta2.py
new file mode 100644
index 00000000..f91bb3cb
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp32s3beta2.py
@@ -0,0 +1,37 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from .esp32s3 import ESP32S3ROM
+
+
+class ESP32S3BETA2ROM(ESP32S3ROM):
+ CHIP_NAME = "ESP32-S3(beta2)"
+ IMAGE_CHIP_ID = 4
+
+ CHIP_DETECT_MAGIC_VALUE = [0xEB004136]
+
+ EFUSE_BASE = 0x6001A000 # BLOCK0 read base address
+
+
+class ESP32S3BETA2StubLoader(ESP32S3BETA2ROM):
+ """Access class for ESP32S3 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32S3BETA2ROM.STUB_CLASS = ESP32S3BETA2StubLoader
diff --git a/mixly/tools/python/esptool/targets/esp8266.py b/mixly/tools/python/esptool/targets/esp8266.py
new file mode 100644
index 00000000..9f8d7c17
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/esp8266.py
@@ -0,0 +1,202 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from ..loader import ESPLoader
+from ..util import FatalError, NotSupportedError
+
+
+class ESP8266ROM(ESPLoader):
+ """Access class for ESP8266 ROM bootloader"""
+
+ CHIP_NAME = "ESP8266"
+ IS_STUB = False
+
+ CHIP_DETECT_MAGIC_VALUE = [0xFFF0C101]
+
+ # OTP ROM addresses
+ ESP_OTP_MAC0 = 0x3FF00050
+ ESP_OTP_MAC1 = 0x3FF00054
+ ESP_OTP_MAC3 = 0x3FF0005C
+
+ SPI_REG_BASE = 0x60000200
+ SPI_USR_OFFS = 0x1C
+ SPI_USR1_OFFS = 0x20
+ SPI_USR2_OFFS = 0x24
+ SPI_MOSI_DLEN_OFFS = None
+ SPI_MISO_DLEN_OFFS = None
+ SPI_W0_OFFS = 0x40
+
+ UART_CLKDIV_REG = 0x60000014
+
+ XTAL_CLK_DIVIDER = 2
+
+ FLASH_SIZES = {
+ "512KB": 0x00,
+ "256KB": 0x10,
+ "1MB": 0x20,
+ "2MB": 0x30,
+ "4MB": 0x40,
+ "2MB-c1": 0x50,
+ "4MB-c1": 0x60,
+ "8MB": 0x80,
+ "16MB": 0x90,
+ }
+
+ FLASH_FREQUENCY = {
+ "80m": 0xF,
+ "40m": 0x0,
+ "26m": 0x1,
+ "20m": 0x2,
+ }
+
+ BOOTLOADER_FLASH_OFFSET = 0
+
+ MEMORY_MAP = [
+ [0x3FF00000, 0x3FF00010, "DPORT"],
+ [0x3FFE8000, 0x40000000, "DRAM"],
+ [0x40100000, 0x40108000, "IRAM"],
+ [0x40201010, 0x402E1010, "IROM"],
+ ]
+
+ UF2_FAMILY_ID = 0x7EAB61ED
+
+ def get_efuses(self):
+ # Return the 128 bits of ESP8266 efuse as a single Python integer
+ result = self.read_reg(0x3FF0005C) << 96
+ result |= self.read_reg(0x3FF00058) << 64
+ result |= self.read_reg(0x3FF00054) << 32
+ result |= self.read_reg(0x3FF00050)
+ return result
+
+ def _get_flash_size(self, efuses):
+ # rX_Y = EFUSE_DATA_OUTX[Y]
+ r0_4 = (efuses & (1 << 4)) != 0
+ r3_25 = (efuses & (1 << 121)) != 0
+ r3_26 = (efuses & (1 << 122)) != 0
+ r3_27 = (efuses & (1 << 123)) != 0
+
+ if r0_4 and not r3_25:
+ if not r3_27 and not r3_26:
+ return 1
+ elif not r3_27 and r3_26:
+ return 2
+ if not r0_4 and r3_25:
+ if not r3_27 and not r3_26:
+ return 2
+ elif not r3_27 and r3_26:
+ return 4
+ return -1
+
+ def get_chip_description(self):
+ efuses = self.get_efuses()
+ is_8285 = (
+ efuses & ((1 << 4) | 1 << 80)
+ ) != 0 # One or the other efuse bit is set for ESP8285
+ if is_8285:
+ flash_size = self._get_flash_size(efuses)
+ max_temp = (
+ efuses & (1 << 5)
+ ) != 0 # This efuse bit identifies the max flash temperature
+ chip_name = {
+ 1: "ESP8285H08" if max_temp else "ESP8285N08",
+ 2: "ESP8285H16" if max_temp else "ESP8285N16",
+ }.get(flash_size, "ESP8285")
+ return chip_name
+ return "ESP8266EX"
+
+ def get_chip_features(self):
+ features = ["WiFi"]
+ if "ESP8285" in self.get_chip_description():
+ features += ["Embedded Flash"]
+ return features
+
+ def flash_spi_attach(self, hspi_arg):
+ if self.IS_STUB:
+ super(ESP8266ROM, self).flash_spi_attach(hspi_arg)
+ else:
+ # ESP8266 ROM has no flash_spi_attach command in serial protocol,
+ # but flash_begin will do it
+ self.flash_begin(0, 0)
+
+ def flash_set_parameters(self, size):
+ # not implemented in ROM, but OK to silently skip for ROM
+ if self.IS_STUB:
+ super(ESP8266ROM, self).flash_set_parameters(size)
+
+ def chip_id(self):
+ """
+ Read Chip ID from efuse - the equivalent of the SDK system_get_chip_id() func
+ """
+ id0 = self.read_reg(self.ESP_OTP_MAC0)
+ id1 = self.read_reg(self.ESP_OTP_MAC1)
+ return (id0 >> 24) | ((id1 & 0xFFFFFF) << 8)
+
+ def read_mac(self, mac_type="BASE_MAC"):
+ """Read MAC from OTP ROM"""
+ if mac_type != "BASE_MAC":
+ return None
+ mac0 = self.read_reg(self.ESP_OTP_MAC0)
+ mac1 = self.read_reg(self.ESP_OTP_MAC1)
+ mac3 = self.read_reg(self.ESP_OTP_MAC3)
+ if mac3 != 0:
+ oui = ((mac3 >> 16) & 0xFF, (mac3 >> 8) & 0xFF, mac3 & 0xFF)
+ elif ((mac1 >> 16) & 0xFF) == 0:
+ oui = (0x18, 0xFE, 0x34)
+ elif ((mac1 >> 16) & 0xFF) == 1:
+ oui = (0xAC, 0xD0, 0x74)
+ else:
+ raise FatalError("Unknown OUI")
+ return oui + ((mac1 >> 8) & 0xFF, mac1 & 0xFF, (mac0 >> 24) & 0xFF)
+
+ def get_erase_size(self, offset, size):
+ """Calculate an erase size given a specific size in bytes.
+
+ Provides a workaround for the bootloader erase bug."""
+
+ sectors_per_block = 16
+ sector_size = self.FLASH_SECTOR_SIZE
+ num_sectors = (size + sector_size - 1) // sector_size
+ start_sector = offset // sector_size
+
+ head_sectors = sectors_per_block - (start_sector % sectors_per_block)
+ if num_sectors < head_sectors:
+ head_sectors = num_sectors
+
+ if num_sectors < 2 * head_sectors:
+ return (num_sectors + 1) // 2 * sector_size
+ else:
+ return (num_sectors - head_sectors) * sector_size
+
+ def get_flash_voltage(self):
+ pass # not supported on ESP8266
+
+ def override_vddsdio(self, new_voltage):
+ raise NotSupportedError(self, "Overriding VDDSDIO")
+
+ def check_spi_connection(self, spi_connection):
+ raise NotSupportedError(self, "Setting --spi-connection")
+
+ def get_secure_boot_enabled(self):
+ return False # ESP8266 doesn't have security features
+
+
+class ESP8266StubLoader(ESP8266ROM):
+ """Access class for ESP8266 stub loader, runs on top of ROM."""
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+ def get_erase_size(self, offset, size):
+ return size # stub doesn't have same size bug as ROM loader
+
+
+ESP8266ROM.STUB_CLASS = ESP8266StubLoader
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/README.md b/mixly/tools/python/esptool/targets/stub_flasher/1/README.md
new file mode 100644
index 00000000..44e3ca31
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/README.md
@@ -0,0 +1,3 @@
+# Licensing
+
+The binaries in JSON format distributed in this directory are released as Free Software under GNU General Public License Version 2 or later. They were released at https://github.com/espressif/esptool-legacy-flasher-stub/releases/tag/v1.3.0 from where the sources can be obtained.
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32.json
new file mode 100644
index 00000000..56221e30
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1074521580,
+ "text": "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",
+ "text_start": 1074520064,
+ "data": "DMD8P+znC0B/6AtAZ+0LQAbpC0Cf6AtABukLQGXpC0CC6gtA9OoLQJ3qC0CV5wtAGuoLQHTqC0CI6QtAGOsLQLDpC0AY6wtAbegLQMroC0AG6QtAZekLQIXoC0DI6wtAKe0LQLjmC0BL7QtAuOYLQLjmC0C45gtAuOYLQLjmC0C45gtAuOYLQLjmC0Bv6wtAuOYLQEnsC0Ap7QtA",
+ "data_start": 1073605544,
+ "bss_start": 1073528832
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c2.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c2.json
new file mode 100644
index 00000000..f10ec7b4
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c2.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1077413304,
+ "text": "ARG3BwBgTsaDqYcASsg3Sco/JspSxAbOIsy3BABgfVoTCQkAwEwTdPQ/DeDyQGJEI6g0AUJJ0kSySSJKBWGCgIhAgycJABN19Q+Cl30U4xlE/8m/EwcADJRBqodjGOUAhUeFxiOgBQB5VYKABUdjh+YACUZjjcYAfVWCgEIFEwewDUGFY5XnAolHnMH1t5MGwA1jFtUAmMETBQAMgoCTBtANfVVjldcAmMETBbANgoC3dcs/QRGThQW6BsZhP2NFBQa3d8s/k4eHsQOnBwgD1kcIE3X1D5MGFgDCBsGCI5LXCDKXIwCnAAPXRwiRZ5OHBwRjHvcCN/fKPxMHh7GhZ7qXA6YHCLc2yz+3d8s/k4eHsZOGhrVjH+YAI6bHCCOg1wgjkgcIIaD5V+MG9fyyQEEBgoAjptcII6DnCN23NycAYHxLnYv1/zc3AGB8S52L9f+CgEERBsbdN7cnAGAjpgcCNwcACJjDmEN9/8hXskATRfX/BYlBAYKAQREGxtk/fd03BwBAtycAYJjDNycAYBxD/f+yQEEBgoBBESLEN8TKP5MHxABKwAOpBwEGxibCYwoJBEU3OcW9RxMExACBRGPWJwEERL2Ik7QUAH03hT8cRDcGgAATl8cAmeA3BgABt/b/AHWPtyYAYNjCkMKYQn3/QUeR4AVHMwnpQLqXIygkARzEskAiRJJEAklBAYKAQREGxhMHAAxjEOUCEwWwDZcAyP/ngIDjEwXADbJAQQEXA8j/ZwCD4hMHsA3jGOX+lwDI/+eAgOETBdANxbdBESLEJsIGxiqEswS1AGMXlACyQCJEkkRBAYKAA0UEAAUERTfttxMFAAwXA8j/ZwAD3nVxJsPO3v10hWn9cpOEhPqThwkHIsVKwdLc1tqmlwbHFpGzhCcAKokmhS6ElzDI/+eAgJOThwkHBWqKl7OKR0Ep5AVnfXUTBIX5kwcHB6KXM4QnABMFhfqTBwcHqpeihTOFJwCXMMj/54CAkCKFwUW5PwFFhWIWkbpAKkSaRApJ9llmWtZaSWGCgKKJY3OKAIVpTobWhUqFlwDI/+eAQOITdfUPAe1OhtaFJoWXMMj/54DAi06ZMwQ0QVm3EwUwBlW/cXH9ck7PUs1Wy17HBtci1SbTStFayWLFZsNqwe7eqokWkRMFAAIuirKKtosCwpcAyP/ngEBIhWdj7FcRhWR9dBMEhPqThwQHopczhCcAIoWXMMj/54AghX17Eww7+ZMMi/kThwQHk4cEB2KX5pcBSTMMJwCzjCcAEk1je00JY3GpA3mgfTWmhYgYSTVdNSaGjBgihZcwyP/ngCCBppkmmWN1SQOzB6lBY/F3A7MEKkFj85oA1oQmhowYToWXAMj/54Dg0xN19Q9V3QLEgUR5XY1NowEBAGKFlwDI/+eAYMR9+QNFMQDmhS0xY04FAOPinf6FZ5OHBweml4qX2pcjiqf4hQT5t+MWpf2RR+OG9PYFZ311kwcHBxMEhfmilzOEJwATBYX6kwcHB6qXM4UnAKKFlyDI/+eAgHflOyKFwUXxM8U7EwUAApcAyP/ngOA2hWIWkbpQKlSaVApZ+klqStpKSku6SypMmkwKTfZdTWGCgAERBs4izFExNwTOP2wAEwVE/5cAyP/ngKDKqocFRZXnskeT9wcgPsZ5OTcnAGAcR7cGQAATBUT/1Y8cx7JFlwDI/+eAIMgzNaAA8kBiRAVhgoBBEbfHyj8GxpOHxwAFRyOA5wAT18UAmMcFZ30XzMPIx/mNOpWqlbGBjMsjqgcAQTcZwRMFUAyyQEEBgoABESLMN8TKP5MHxAAmysRHTsYGzkrIqokTBMQAY/OVAK6EqcADKUQAJpkTWckAHEhjVfAAHERjXvkC4T593UhAJobOhZcAyP/ngCC7E3X1DwHFkwdADFzIXECml1zAXESFj1zE8kBiRNJEQkmySQVhgoDdNm2/t1dBSRlxk4f3hAFFPs6G3qLcptrK2M7W0tTW0trQ3s7izObK6sjuxpcAyP/ngICtt0fKPzd3yz+ThwcAEweHumPg5xSlOZFFaAixMYU5t/fKP5OHh7EhZz6XIyD3CLcFOEC3BzhAAUaThwcLk4UFADdJyj8VRSMg+QCXAMj/54DgGzcHAGBcRxMFAAK3xMo/k+cXEFzHlwDI/+eAoBq3RwBgiF+BRbd5yz9xiWEVEzUVAJcAyP/ngOCwwWf9FxMHABCFZkFmtwUAAQFFk4TEALdKyj8NapcAyP/ngOCrk4mJsRMJCQATi8oAJpqDp8kI9d+Dq8kIhUcjpgkIIwLxAoPHGwAJRyMT4QKjAvECAtRNR2OL5wZRR2OJ5wYpR2Of5wCDxzsAA8crAKIH2Y8RR2OW5wCDp4sAnEM+1EE2oUVIEJE+g8c7AAPHKwCiB9mPEWdBB2N+9wITBbANlwDI/+eAQJQTBcANlwDI/+eAgJMTBeAOlwDI/+eAwJKBNr23I6AHAJEHbb3JRyMT8QJ9twPHGwDRRmPn5gKFRmPm5gABTBME8A+dqHkXE3f3D8lG4+jm/rd2yz8KB5OGxro2lxhDAoeTBgcDk/b2DxFG42nW/BMH9wITd/cPjUZj7uYIt3bLPwoHk4aGvzaXGEMChxMHQAJjmucQAtQdRAFFlwDI/+eAIIoBRYE8TTxFPKFFSBB9FEk0ffABTAFEE3X0DyU8E3X8Dw08UTzjEQTsg8cbAElHY2X3MAlH43n36vUXk/f3Dz1H42P36jd3yz+KBxMHh8C6l5xDgocFRJ3rcBCBRQFFlwDI/+eAQIkd4dFFaBAVNAFEMagFRIHvlwDI/+eAwI0zNKAAKaAhR2OF5wAFRAFMYbcDrIsAA6TLALNnjADSB/X3mTll9cFsIpz9HH19MwWMQF3cs3eVAZXjwWwzBYxAY+aMAv18MwWMQF3QMYGXAMj/54Bgil35ZpT1tzGBlwDI/+eAYIld8WqU0bdBgZcAyP/ngKCIWfkzBJRBwbchR+OK5/ABTBMEAAw5t0FHzb9BRwVE453n9oOlywADpYsAVTK5v0FHBUTjk+f2A6cLAZFnY+jnHoOlSwEDpYsAMTGBt0FHBUTjlOf0g6cLARFnY2n3HAOnywCDpUsBA6WLADOE5wLdNiOsBAAjJIqwCb8DxwQAYwMHFAOniwDBFxMEAAxjE/cAwEgBR5MG8A5jRvcCg8dbAAPHSwABTKIH2Y8Dx2sAQgddj4PHewDiB9mP44T25hMEEAyFtTOG6wADRoYBBQexjuG3g8cEAP3H3ERjnQcUwEgjgAQAVb1hR2OW5wKDp8sBA6eLAYOmSwEDpgsBg6XLAAOliwCX8Mf/54BgeSqMMzSgAAG9AUwFRCm1EUcFROOd5+a3lwBgtENld30XBWb5jtGOA6WLALTDtEeBRfmO0Y60x/RD+Y7RjvTD1F91j1GP2N+X8Mf/54BAdwW1E/f3AOMXB+qT3EcAE4SLAAFMfV3jd5zbSESX8Mf/54DAYRhEVEAQQPmOYwenARxCE0f3/32P2Y4UwgUMQQTZvxFHtbVBRwVE45rn3oOniwADp0sBIyT5ACMi6QDJs4MlSQDBF5Hlic8BTBMEYAyhuwMniQBjZvcGE/c3AOMbB+IDKIkAAUYBRzMF6ECzhuUAY2n3AOMHBtIjJKkAIyLZAA2zM4brABBOEQeQwgVG6b8hRwVE45Tn2AMkiQAZwBMEgAwjJAkAIyIJADM0gAC9swFMEwQgDMW5AUwTBIAM5bEBTBMEkAzFsRMHIA1jg+cMEwdADeOR57oDxDsAg8crACIEXYyX8Mf/54BgXwOsxABBFGNzhAEijOMPDLbAQGKUMYCcSGNV8ACcRGNa9Arv8I/hdd3IQGKGk4WLAZfwx//ngGBbAcWTB0AM3MjcQOKX3MDcRLOHh0HcxJfwx//ngEBaFb4JZRMFBXEDrMsAA6SLAJfwx//ngEBMtwcAYNhLtwYAAcEWk1dHARIHdY+9i9mPs4eHAwFFs9WHApfwx//ngOBMEwWAPpfwx//ngOBI3bSDpksBA6YLAYOlywADpYsA7/Av98G8g8U7AIPHKwAThYsBogXdjcEVqTptvO/w79qBtwPEOwCDxysAE4yLASIEXYzcREEUxeORR4VLY/6HCJMHkAzcyHm0A6cNACLQBUizh+xAPtaDJ4qwY3P0AA1IQsY6xO/wb9YiRzJIN8XKP+KFfBCThsoAEBATBUUCl/DH/+eA4Ek398o/kwjHAIJXA6eIsIOlDQAdjB2PPpyyVyOk6LCqi76VI6C9AJOHygCdjQHFoWdjlvUAWoVdOCOgbQEJxNxEmcPjQHD5Y98LAJMHcAyFv4VLt33LP7fMyj+TjY26k4zMAOm/45ULntxE44IHnpMHgAyxt4OniwDjmwecAUWX8Mf/54DAOQllEwUFcZfwx//ngCA2l/DH/+eA4DlNugOkywDjBgSaAUWX8Mf/54AgNxMFgD6X8Mf/54CgMwKUQbr2UGZU1lRGWbZZJlqWWgZb9ktmTNZMRk22TQlhgoA=",
+ "text_start": 1077411840,
+ "data": "DEDKP+AIOEAsCThAhAk4QFIKOEC+CjhAbAo4QKgHOEAOCjhATgo4QJgJOEBYBzhAzAk4QFgHOEC6CDhA/gg4QCwJOECECThAzAg4QBIIOEBCCDhAyAg4QBYNOEAsCThA1gs4QMoMOECkBjhA9Aw4QKQGOECkBjhApAY4QKQGOECkBjhApAY4QKQGOECkBjhAcgs4QKQGOEDyCzhAygw4QA==",
+ "data_start": 1070295976,
+ "bss_start": 1070219264
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c3.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c3.json
new file mode 100644
index 00000000..788ae646
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c3.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1077413584,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "GEDIP8AKOEAQCzhAaAs4QDYMOECiDDhAUAw4QHIJOEDyCzhAMgw4QHwLOEAiCThAsAs4QCIJOECaCjhA4Ao4QBALOEBoCzhArAo4QNYJOEAgCjhAqAo4QPoOOEAQCzhAug04QLIOOEBiCDhA2g44QGIIOEBiCDhAYgg4QGIIOEBiCDhAYgg4QGIIOEBiCDhAVg04QGIIOEDYDThAsg44QA==",
+ "data_start": 1070164916,
+ "bss_start": 1070088192
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c5.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c5.json
new file mode 100644
index 00000000..871a95d5
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c5.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1082132164,
+ "text": "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",
+ "text_start": 1082130432,
+ "data": "FACEQG4KgEC+CoBAFguAQOQLgEBQDIBA/guAQDoJgECgC4BA4AuAQCoLgEDqCIBAXguAQOoIgEBICoBAjgqAQL4KgEAWC4BAWgqAQJ4JgEDOCYBAVgqAQKgOgEC+CoBAaA2AQGAOgEAqCIBAiA6AQCoIgEAqCIBAKgiAQCoIgEAqCIBAKgiAQCoIgEAqCIBABA2AQCoIgECGDYBAYA6AQA==",
+ "data_start": 1082469296,
+ "bss_start": 1082392576
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c5beta3.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c5beta3.json
new file mode 100644
index 00000000..c41e549c
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c5beta3.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1082131910,
+ "text": "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",
+ "text_start": 1082130432,
+ "data": "DACEQO4IgEA6CYBAkgmAQGAKgEDMCoBAegqAQLYHgEAcCoBAXAqAQKYJgEBmB4BA2gmAQGYHgEDICIBADAmAQDoJgECSCYBA2giAQCAIgEBQCIBA1giAQCQNgEA6CYBA5AuAQNgMgECyBoBAAg2AQLIGgECyBoBAsgaAQLIGgECyBoBAsgaAQLIGgECyBoBAgAuAQLIGgEAADIBA2AyAQA==",
+ "data_start": 1082469288,
+ "bss_start": 1082392576
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c6.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c6.json
new file mode 100644
index 00000000..b903b352
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c6.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1082132164,
+ "text": "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",
+ "text_start": 1082130432,
+ "data": "FACEQHIKgEDCCoBAGguAQOgLgEBUDIBAAgyAQD4JgECkC4BA5AuAQC4LgEDuCIBAYguAQO4IgEBMCoBAkgqAQMIKgEAaC4BAXgqAQKIJgEDSCYBAWgqAQKwOgEDCCoBAbA2AQGQOgEAuCIBAjA6AQC4IgEAuCIBALgiAQC4IgEAuCIBALgiAQC4IgEAuCIBACA2AQC4IgECKDYBAZA6AQA==",
+ "data_start": 1082469296,
+ "bss_start": 1082392576
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c61.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c61.json
new file mode 100644
index 00000000..2a95414c
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c61.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1082132164,
+ "text": "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",
+ "text_start": 1082130432,
+ "data": "FECAQHQKgEDECoBAHAuAQOoLgEBWDIBABAyAQEAJgECmC4BA5guAQDALgEDwCIBAZAuAQPAIgEBOCoBAlAqAQMQKgEAcC4BAYAqAQKQJgEDUCYBAXAqAQK4OgEDECoBAbg2AQGYOgEAwCIBAjg6AQDAIgEAwCIBAMAiAQDAIgEAwCIBAMAiAQDAIgEAwCIBACg2AQDAIgECMDYBAZg6AQA==",
+ "data_start": 1082223536,
+ "bss_start": 1082146816
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c6beta.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c6beta.json
new file mode 100644
index 00000000..7fd5c0ec
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32c6beta.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1077413318,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "DEDIP/gIOEBECThAnAk4QGoKOEDWCjhAhAo4QMAHOEAmCjhAZgo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QC4NOEBECThA7gs4QOIMOEC8BjhADA04QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAigs4QLwGOEAKDDhA4gw4QA==",
+ "data_start": 1070164904,
+ "bss_start": 1070088192
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32h2.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32h2.json
new file mode 100644
index 00000000..24964cde
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32h2.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1082132164,
+ "text": "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",
+ "text_start": 1082130432,
+ "data": "FACDQHIKgEDCCoBAGguAQOgLgEBUDIBAAgyAQD4JgECkC4BA5AuAQC4LgEDuCIBAYguAQO4IgEBMCoBAkgqAQMIKgEAaC4BAXgqAQKIJgEDSCYBAWgqAQKwOgEDCCoBAbA2AQGQOgEAuCIBAjA6AQC4IgEAuCIBALgiAQC4IgEAuCIBALgiAQC4IgEAuCIBACA2AQC4IgECKDYBAZA6AQA==",
+ "data_start": 1082403760,
+ "bss_start": 1082327040
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32h2beta1.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32h2beta1.json
new file mode 100644
index 00000000..03e110c5
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32h2beta1.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1077413318,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "DEDIP/gIOEBECThAnAk4QGoKOEDWCjhAhAo4QMAHOEAmCjhAZgo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QC4NOEBECThA7gs4QOIMOEC8BjhADA04QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAigs4QLwGOEAKDDhA4gw4QA==",
+ "data_start": 1070164904,
+ "bss_start": 1070088192
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32h2beta2.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32h2beta2.json
new file mode 100644
index 00000000..ce3afe7a
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32h2beta2.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1077413318,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "DEDIP/gIOEBECThAnAk4QGoKOEDWCjhAhAo4QMAHOEAmCjhAZgo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QC4NOEBECThA7gs4QOIMOEC8BjhADA04QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAigs4QLwGOEAKDDhA4gw4QA==",
+ "data_start": 1070164904,
+ "bss_start": 1070088192
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32p4.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32p4.json
new file mode 100644
index 00000000..6f37e91b
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32p4.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1341195918,
+ "text": "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",
+ "text_start": 1341194240,
+ "data": "EAD1TwYK8U9WCvFPrgrxT4QL8U/wC/FPngvxT9QI8U9AC/FPgAvxT8IK8U+ECPFP9grxT4QI8U/gCfFPJgrxT1YK8U+uCvFP8gnxTzgJ8U9oCfFP7gnxT0AO8U9WCvFPCA3xTwAO8U/EB/FPJA7xT8QH8U/EB/FPxAfxT8QH8U/EB/FPxAfxT8QH8U/EB/FPpAzxT8QH8U8mDfFPAA7xTw==",
+ "data_start": 1341533100,
+ "bss_start": 1341456384
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32s2.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32s2.json
new file mode 100644
index 00000000..68de6138
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32s2.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1073907716,
+ "text": "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",
+ "text_start": 1073905664,
+ "data": "WAD9P0uLAkDdiwJA8pACQGaMAkD+iwJAZowCQMWMAkDejQJAUY4CQPmNAkDVigJAd40CQNCNAkDojAJAdI4CQBCNAkB0jgJAy4sCQCqMAkBmjAJAxYwCQOOLAkAXiwJAN48CQKqQAkDqiQJA0ZACQOqJAkDqiQJA6okCQOqJAkDqiQJA6okCQOqJAkDqiQJA1I4CQOqJAkDJjwJAqpACQA==",
+ "data_start": 1073622012,
+ "bss_start": 1073545216
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32s3.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32s3.json
new file mode 100644
index 00000000..484a8832
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32s3.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1077381760,
+ "text": "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",
+ "text_start": 1077379072,
+ "data": "XADKP16ON0AzjzdAR5Q3QL2PN0BTjzdAvY83QB2QN0A6kTdArJE3QFWRN0DpjTdA0JA3QCyRN0BAkDdA0JE3QGiQN0DQkTdAIY83QH6PN0C9jzdAHZA3QDmPN0AqjjdAkJI3QA2UN0AAjTdALZQ3QACNN0AAjTdAAI03QACNN0AAjTdAAI03QACNN0AAjTdAKpI3QACNN0AlkzdADZQ3QAQInwAAAAAAAAAYAQQIBQAAAAAAAAAIAQQIBgAAAAAAAAAAAQQIIQAAAAAAIAAAEQQI3AAAAAAAIAAAEQQIDAAAAAAAIAAAAQQIEgAAAAAAIAAAESAoDAAQAQAA",
+ "data_start": 1070279676,
+ "bss_start": 1070202880
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp32s3beta2.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32s3beta2.json
new file mode 100644
index 00000000..da770f7b
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp32s3beta2.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1077380596,
+ "text": "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",
+ "text_start": 1077379072,
+ "data": "DADKPxeIN0CriDdAw403QDeJN0DLiDdAN4k3QJaJN0C2ijdAKIs3QNGKN0ChhzdASIo3QKiKN0C5iTdATIs3QOGJN0BMizdAmYg3QPiIN0A3iTdAlok3QLGIN0DjhzdABIw3QIWNN0DAhjdAp403QMCGN0DAhjdAwIY3QMCGN0DAhjdAwIY3QMCGN0DAhjdAqYs3QMCGN0CZjDdAhY03QA==",
+ "data_start": 1070279592,
+ "bss_start": 1070202880
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/1/esp8266.json b/mixly/tools/python/esptool/targets/stub_flasher/1/esp8266.json
new file mode 100644
index 00000000..f68ffef9
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/1/esp8266.json
@@ -0,0 +1,8 @@
+{
+ "entry": 1074843652,
+ "text": "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",
+ "text_start": 1074843648,
+ "data": "CIH+PwUFBAACAwcAAwMLANTXEEAL2BBAOdgQQNbYEECF5xBAOtkQQJDZEEDc2RBAhecQQKLaEEAf2xBA4NsQQIXnEECF5xBAeNwQQIXnEEBV3xBAHOAQQFfgEECF5xBAhecQQPPgEECF5xBA2+EQQIHiEEDA4xBAf+QQQFDlEECF5xBAhecQQIXnEECF5xBAfuYQQIXnEEB05xBAsN0QQKnYEEDC5RBAydoQQBvaEECF5xBACOcQQE/nEECF5xBAhecQQIXnEECF5xBAhecQQIXnEECF5xBAhecQQELaEEB/2hBA2uUQQAEAAAACAAAAAwAAAAQAAAAFAAAABwAAAAkAAAANAAAAEQAAABkAAAAhAAAAMQAAAEEAAABhAAAAgQAAAMEAAAABAQAAgQEAAAECAAABAwAAAQQAAAEGAAABCAAAAQwAAAEQAAABGAAAASAAAAEwAAABQAAAAWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAgAAAAIAAAADAAAAAwAAAAQAAAAEAAAABQAAAAUAAAAGAAAABgAAAAcAAAAHAAAACAAAAAgAAAAJAAAACQAAAAoAAAAKAAAACwAAAAsAAAAMAAAADAAAAA0AAAANAAAAAAAAAAAAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAANAAAADwAAABEAAAATAAAAFwAAABsAAAAfAAAAIwAAACsAAAAzAAAAOwAAAEMAAABTAAAAYwAAAHMAAACDAAAAowAAAMMAAADjAAAAAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAAEAAAABAAAAAQAAAAEAAAABQAAAAUAAAAFAAAABQAAAAAAAAAAAAAAAAAAABAREgAIBwkGCgULBAwDDQIOAQ8AAQEAAAEAAAAEAAAA",
+ "data_start": 1073720488,
+ "bss_start": 1073643776
+}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/2/LICENSE-APACHE b/mixly/tools/python/esptool/targets/stub_flasher/2/LICENSE-APACHE
new file mode 100644
index 00000000..f8e5e5ea
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/2/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/2/LICENSE-MIT b/mixly/tools/python/esptool/targets/stub_flasher/2/LICENSE-MIT
new file mode 100644
index 00000000..3e8a0853
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/2/LICENSE-MIT
@@ -0,0 +1,25 @@
+Copyright 2022 esp-rs
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/2/README.md b/mixly/tools/python/esptool/targets/stub_flasher/2/README.md
new file mode 100644
index 00000000..eae371e4
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/2/README.md
@@ -0,0 +1,3 @@
+# Licensing
+
+The binaries in JSON format distributed in this directory are dual licensed under the Apache License Version 2.0 or the MIT license. They were released at https://github.com/esp-rs/esp-flasher-stub/releases/tag/v0.3.0 from where the sources can be obtained.
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/2/esp32.json b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32.json
new file mode 100644
index 00000000..8e6bd053
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32.json
@@ -0,0 +1 @@
+{"entry":1074274996,"text":"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GTAMEzIhGjAEEzIhGzAQEzIhHDAREzIhHTAgEzIhHjAhEzIhHzAiEzIhIDAjEzIhITDq8zIhIjDr8zIhIzDs8zIhJDDo8zIhJTDp8wMBJhMBJyMBKDMBKUMBKlMBK2MBLHMBLYMBLpMBL6MBMLMBMcMBMtMBM+MBNPMBNSIhBDIhBUIhBlIhB2IhCHIhCYIhCpIhC6IhDLIhDcIhDtIhD+IhEPIhEYAAAPBBABABIBLR/wJhAwDRSQDmAwJhAQDoAwJhEwDuAwJhFACxAwJhAADBSQDRAwJhAoXe/wF5/gDmExAgAGIhEyZGCBBxIFUoAEYEAAF0/gDmExAgAGKgARBxIJWd/4Xt/wIhAQDmEwIhAACxEwIhAhIhAxAgAAAwAPBBABABIBLR/wJhAwDRSQDmAwJhAQDoAwJhEwDuAwJhFADAAwJhAADBSQDXAwJhAgXX/2IhExBxIJUhAAXo/wIhAQDmEwIhAACxEwIhAhIhAxAgAAAyAPBBAAAQASAS0f8CYQMA0UkAwgMCYQEAsgMCYQAAwUkA0gMCYQJF0v8BSv4A5hMQIABioAIQcSDVkv/F4v8CIQEAwhMCIQAAshMCIQISIQMQIAAQMgDwQQAQASAS0f8CYQMA0UkAwwMCYQEAswMCYQAAwUkA0wMCYQIFzf8BM/4A5hMQIABioAMQcSCVjf+F3f8CIQEAwxMCIQAAsxMCIQISIQMQIAAQMwDwQQAQASAS0f8CYQMA0UkAxAMCYQEAtAMCYQAAwUkA1AMCYQLFx/8BIf4A5hMQIABioAQQcSDVwv9F2P8CIQEAxBMCIQAAtBMCIQISIQMQIAAQNADwQQAQASAS0f8CYQMA0UkAxQMCYQEAtQMCYQAAwUkA1QMCYQKFwv8BDf4A5hMQIABioAUQcSCVvv8F0/8CIQEAxRMCIQAAtRMCIQISIQMQIAAQNQDwQQAQASAS0f8CYQMA0UkAxgMCYQEAtgMCYQAAwUkA1gMCYQJFvf8B+f0A5hMQIABioAYQcSBVuv/Fzf8CIQEAxhMCIQAAthMCIQISIQMQIAAQNgDwQQAQASAS0f8CYQMA0UkAxwMCYQEAtwMCYQAAwUkA1wMCYQIFuP8B5f0A5hMQIABioAcQcSAVtv+FyP8CIQEAxxMCIQAAtxMCIQISIQMQIAAQNwDwQQA2QQCBv/2tAr0D4AgAHfAANkEA8EEAAADkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhAAAAAAAAAAADkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEDkJwhA5CcIQOQnCEA=","text_start":1074266112,"data":"5hMIQO0UCEBGFQhA5hMIQKsVCEDtFAhAGhYIQFEWCEChFghAABcIQKkeCEAMFwhAqR4IQD4XCEDmEwhA7RQIQEYVCEDoFwhAlRwIQN4UCEAWGQhAaRkIQKIVCEDtFAhATisIQA4pCEAGLAhABiwIQAYsCEA5KwhABiwIQAYsCEA/KwhARSsIQEsrCEDA29zb3QA6ADsAPAA9AD4APwBAAEEAAAAAAAAA/zcGAAAAOAAAiMAoAAAAUwAAAYQAAAAAAEAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAwAAAAEAAAABAAAAAAAAAAMAAAAAAAAAAQAAAAEAAAACAAAAAgAAAAIAAAADAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAwAAAAAAAAAAAAAAAAABAAIAAwAEAAUABgAHAAgACQBFAEUADAANAA4ADwAQABEAEgATABQAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAAxADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQAAAAAAQAA","data_start":1073414144}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/2/esp32c2.json b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32c2.json
new file mode 100644
index 00000000..72c2ac6c
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32c2.json
@@ -0,0 +1 @@
+{"entry":1077411840,"text":"twA4QGeAgAAXFZP/EwVFsJfVk/+ThYXyY1e1AIFGFMERBeNOtf6BQAFBgUEBQoFCAUOBQwFEgUQBRYFFAUaBRgFHgUcBSIFIAUmBSQFKgUoBS4FLAUyBTAFNgU0BToFOAU+BT5cRk/+TgYEq8yNA8bcCAACTggIAY/RyAG8AgAQXAZX/EwEB+JFCMwFRQBNxAf8KhG8AQABBEQbGlxAAAOeAQHWXEAAA54DATTflyz+3xa3ek4XlqyMstfKXAAAA54DgTm8AAABBEQbGlwDI/+eAAAcTNRUAfRUTdTUGskBBAYKAQREGxoNFFQADRgUAg0YlAANHNQCiBdGNwgZiB9mOM+i2AINFVQADRkUAg0ZlAANHdQCiBdGNwgZiB9mO1Y0DRpUAg0aFAANHpQCDR7UAIgZVjkIH4gddj1mOg0jVAANHxQCDR+UAg0b1AKIIM+foAMIH4gbdjtmOg0gVAYNCBQEDQyUBg0c1AaIIM+dYAEID4gez52cAXY+DQlUBg0hFAQNDZQEDRXUBogKz5xIBQgNiBTNlZQDJj0KFlwDI/+eA4PoTNRUAfRUTdUX8skBBAYKAsoYuhoFFFwMAAGcAAw5BEQbGIsQmwi6EqoSXAAAA54BAKylGJoWihZcAAADngIArA0W0AINFpAADRsQAg0bUACIFTY1CBuIGs+XGAMmNA0X0AANG5ACDRgQBA0cUASIFUY3CBmIHM2bXAEmOJoWXAAAA54AgJyaFskAiRJJEQQEXAwAAZwDjJAERBs4izCbKSshOxjaJsokuhKqElwAAAOeAICMhRiaFooWXAAAA54BgIyaFzoVKhpcAAADngIAikwWEAAlGJoWXAAAA54CAISaF8kBiRNJEQkmySQVhFwMAAGcAAx85cQbeItwm2krYTtZS1FbSWtBezmLMZspqyG7GqooDKwUBBEU2ijKJrolajGNj2wBSjAOkygCDrUoAJsTilMFrBW1jf5QCExUEAQntswx0AWPqnQETVQQBlwDI/+eAAOQZyUGoE1XEAJcAyP/ngMDiSeGzDKQBI6aaAWaE4+Wc/AFEDWUTDVWwVp2iS96MYwUMBAVl4oRjY6wAhWRjbIoGswWJAGaFJoZjhwkAlwDI/+eAgNspoJcAyP/ngGDeEzUVAH0VE3VFDCMArQAzDJxAJpSmnOMfDPoBRbOFiwDWhozGswWLQDM2uwB9FvGNjMoZoBMFYAPyUGJU0lRCWbJZIlqSWgJb8ktiTNJMQk2yTSFhgoAAAAERBs4izCbKSshOxrKJLokqhCKFlwAAAOeAABQTdfUPEwUF9H31gUQihZcAAADngKASk3X1DxOGVfId4iKFlwAAAOeAYBETdfUPkwVF8pnNEwU18nH5EwWwDWPtNAElqJOFBfSRzWPnNAE1oBMFAAxj8zQDswWZACOApQCFBG23Y+uZAEqFpoXyQGJE0kRCSbJJBWGCgAAAQREGxiLEJsJKwDKJroQqhJcAAADngKACIoWmhUqGlwAAAOeA4AIihbJAIkSSRAJJQQEXAwAAZwCDALcFyz+ThQUKBUYXAwAAZwBjDwERBs4izCbKSshOxlLEMoSuhCqJNwXLP5MJFQo3Bcs/Ewo1ChXOA8UEAJMFBfSByZMFVfKJ6QlGSoXShRmoCUZKhc6FOaCjA6EAkwVxAAVGSoWXAAAA54DACYUEfRRh9PJAYkTSREJJskkiSgVhgoA1zTcVyz8TBcWyxUWqBS6VoUWBR/N3BDAQQVRBA0eFAKGLmcNzoAUwNY5ZjhM2FgBt8oFF83UEMDcVyz8TBcWyRUcqB7MG5QCQQtxCYxX2AIPHhgCFxyOEBgCTBxYAPY8TNxcAfRd9j5jCMpUDRQUAoYmBxaFFc6AFMIKAAAAVwS6WNwUAYLcGgANji8UAWE11j3X/A8cFAIUFGMHjmcX+goAAABFxht+i3abbytnO19LV1tPa0d7P4s3my+rJ7scpZRMFBYIzAaFAAUVzdQQwtxXLPwPG1bAZwm8AMHAFRiGJo4bFsAHFIUVzIAUwN4UAYCBVKFWTdAUImcQFRYVFlxAAAOeAoBgTuRQAE3UEBJM0FQAFRalFlxAAAOeAgAGTFjUBk1XVAClGNoWBRpcQAADngAB1twULAZOFBXazBrUCszW1AjdGQg82hYFGlxAAAOeAAHO2BU2BM2S1ACaFyoWXEAAA54CAEpM0JAITyhQAUoWXEAAA54Cg5pMFcAdj5aUEN4UAYAxBk/X1qwzBN/UEYBMGBYAsQu2ZLMIsQpPlhQAswrcKDGDWhYhFE2VFAIjFEwUQAooEY2yFApMJwA4TCQAHMUQVqJcAyP/ngEAKNwUMYCxNk/UFwCzNLE19dhMG9j/xjSzNCUWTBYAHdaihSRMJMAcTBAAFEwVgBhFGkwawBoFFlyDI/+eAYL4TBWAGCUaBRaKGlyDI/+eAQL0TBWAGDUaBRc6GlyDI/+eAILwTBWAGFUaJRoFFAUemh5cgyP/ngEC7EwVgBhVGmUYRR4FFpoeXIMj/54DguRMFYAYZRoFFyoaXIMj/54BAuBMFYAYlRoVGiUeBRQFHlyDI/+eAYLdWhihO/XVtjRMFBUAozghGcZkFBQjGEwWAB5cAyP/ngMD8AUWXEAAA54Cg1w1lEwWFsrMKoQBShZcQAADngMDTKoSXAMj/54CAjTcFDGAMSZ4FY8cFAAxJNwYAAdGNDMmBRR1GIwDBAhAQ1oaQwhMGBQHQwhMGhQGQxhMGRQHQxnEFiMrzdQQwA0UBAjcGyz8TBkYRCgUqlhBCtwbLP5OGxhOqlpRCNwfLPxMHRxYqlxhDtwfLPw1oEwiIsrMEAQEmlhBCk4fHGD6VCEEcQt2OFMImlxBDFEJ1jaGJCMKBxSFFcyAFMDcFBACTBRURNwUAYAzJAUZQxZ1liQUMyVDFt4UcHBOG9b+zBsQAk4UFwLPVtgITl8UAt/cPAD6XtwdwA12PON3jd9ZCcWYTBgYgs4XFAhIEs1W0AhOWBQFRgvIFoYHRjUzJDFGT5cUADNEMUfWZDNEMUZP19fzBBQzRNyU4QBMFBWu3JThAk4UlpWzBcyVA8eMfBTw3JQxgBURgwYMlhQuT5SUAIyy1CrcEAGCIULcFAARNjYjQiFC3BQQATY2I0IhQtwX8//0VbY2I0IhQtwUCAE2NiNCIUIF1/RVtjYjQPUWX8Mf/54DgbshEE2UVAMjE83UEMDcVyz8TBkWxA0VGARnhIwqGAKGJmcFzYAQw4x8FNA1lEwVVsAwQKWYTBqaAswnBADOMpQA3Fcs/iUUjGrWwEwVFsSMIBQCpRSMJtQANZRMFhbIzBKEAjUQFZRMJBdqZyCKFgUVKhpcAyP/ngICrSpT9FP30DWsTBAuOHWUTBYXyCpWNZZOFhbKKlSKGlwDI/+eAYKk3RQBgCF1xiWEVEzUVAIFFl/DH/+eAoHYjoAoANwUAAdaFyMFBaiOkSgEFZcjFEwUAEIjJEwX6/8jJDWUTBYWyCpWX8P//54BAajcVyz+TBEWxJtBIEBMJgQcTBkAFgUWXAMj/54Cgoh1lEwWF8rMFoQBKhSKGlwDI/+eAgKETBYuTDBAulRMG8ByBRZcAyP/ngOCfEwXwBCOAqgATBYAEo4CqABMFEAQjgaoAEwWQBKOBqgANZRMFhbKzBaEAEUYmhZcAAADngKCYRUkqCQ1lEwWFsgqVgUVKhpcAyP/ngACbEw0K8CFKhUs3Bcs/EwXFBirON1XLPxMFhfMqxjcFyz+TDQUCCWUTBQVxKsIZS2LKAlWNZZOFhbKKlUqGlwAAAOeAYInj70UbKoSujANFFQCDRQQAA0YkAINGNAAiBU2NQgbiBlWOs2SmADP1pAETBQXw4wgFGINFRAADR1QAg0hkAINGdAAT1YQAk5eFAV2NI4B5AaOAqQCjgQkACUYjgckAo4MJACODCQCjggkAI4IJACOECQCjhAkAo4YJACOGCQCjhQkAI4V5AaOICQAjiAkAo4cJAJN69Q8Ther/I4cJAElGY26mCgoFbpUIQQKFEwUADOFFY+m8eJcSAADngiLxs+XGAMmNA0VUAQNGRAGDRmQBA0d0ASIFUY3CBmIH2Y5VjQNG1ACDRsQAA0fkAINH9AAiBlWOQgfiB12PWY6DRpQAA0eEAINHpACDRLQAogbZjsIH4gTFj92OfXdpjzrWKtQ2lSrSMwW2AiraNtgC3KMAfAFBRWOFqgAJRWOZqnATBSAMEWZjYLZwl/DH/+eAQE2qhRMFUAxjlwVuza0ThQrzCgVyRjKVCEEChZfwx//ngCBIqoUTBUAMY5YFbMGtYUVj4KxsJoVjj3prA0UcAGMPBVgTiIz+kwKEAWMECF6zBJQBkwfwDhaFA0YFAAUFsY/jHJX+k/f3D/mjEwUADGODTGmDRYQAJoUJRmONxWYDRRwAYw0FVEJWEwWADGMUBmajAAwAY5QFZgJVqWWThaWAipWX8P//54DgShJFl/DH/+eAQDKX8Mf/54AANoGlLUVjd5V7lxIAAOeCoteBRZfwx//ngGBDHaWBRIVFhYhjnwRgY1S7AB1EGaAThBUAAlWTpXUAk8QVAKllk4WlgIqVl/D//+eAAEWihcm/HWUTBYXyCpVRRoFFl/DH/+eAwG8dZRMFhfIzBqEAAUWBRZfwx//ngOAwYwoFShMFMAxdqxMFAAzhRWPnvFqXEgAA54Ii0VGNg0WUAANGhACDRqQAA0e0AKIF0Y3CBmIH2Y7VjYjBaaMTBQAMwUVj7bxWA0WUAINFhAADRqQAIgXJjUIGg0a0AANKxACDRNQAA0zkAINK9AACVeIGVY4zZLYAqWWThaWAipWX8P//54CgORFlEwWFqZfwx//ngMAgNwUAYEhJYw4EaKIEs+VEAUIM4goz5ooB0Y0TFsUAIYIiBXGBUY0zBbUCs1WFAgFFl/DH/+eAgCATBYA+l/DH/+eAgBxSTCFKYbETBQAMwUVj7LxMwlUTBYAMY5cFTJcSAADngkLBYxUFTANF1ACDRcQAA0bkACIFg0b0AMmNQgYCVeIGVY4zZLYAqWWThaWAipWX8P//54AgLxJFl/DH/+eAgBYClGmhLUVjfJVflxIAAOeCQrwIQSOCqQCTVYUBo4O5AJNVBQEjg7kAIYGjgqkAuakTBQACY+asRJMFJAB5Rh1lEwWl8gqVl/DH/+eAIFYdZRMFhfIKlSMAlQCtRaMAtQAdZRMFBfMKlZfw///ngIAZk3X1D2OYBUARqRMFAAzhRWPivECXEgAA54KCtpcSAADngsK6IgVNjUIG4gZVjjNkpgAdZRMFhfIKlQVmgUWX8Mf/54BgTyllEwXFggqVlyDI/+eAIMRjCAoEHWUTBYXyswWhAAVmIoWX8P//54DAEJN19Q9jlQVQBWXShGNjqgCFZCllEwXFggqVnWWThYXyipUmhpcgyP/ngADAMwqaQCaU4xwK+pcSAADngsKwl/DH/+eAoEgpZRMFRYkKlallk4VFiIqVlyDI/+eAIL0DxKkHPUYpZRMF1YEKlallk4VViIqVl/DH/+eAQEUCVSOJiQCpZZOFpYCKlSlmEwbGgQqWwUaX8P//54BgHiFKfbQCVallk4WlgIqVl/D//+eAYBUTBQAM4UVj5rwulxIAAOeCAqUzZ6YAlxIAAOeCAqZVjrNqpgADRVQBg0VEAQNGZAGDRnQBIgVNjUIG4gZVjpcSAADngoKmOoQiBU2NQgbiBlWOs2ymAB1lEwWF8gqVBWaBRZfwx//ngAA7I6kJACllEwXFggqVlyDI/+eAgK+BRAFMMwVaAyrMIopjfYAKYkUqnGMLCgZj+YQHIotWhGPjSgFShAVlBQVjcqQ8HWUTBYXyswWhAGaFIoaX8P//54CA+ZN19Q9jnQUQAlWdZZOFhfKKlSKGl/D//+eA4C8pZRMFxYIKlZ1lk4WF8oqVIoaXIMj/54AgqDMKikCinKKUWoQZS+MZCvgCVallk4XFgYqVEUaX8P//54CAIg1GY3e2NINFFQADRgUAg0YlAANFNQCiBdGNwgZiBVWNM2y1AONnjPSXEgAA54LClJfwx//ngKAsKWUTBUWJCpWpZZOFRYiKlZcgyP/ngCChAlWpZZOFRYiKlUFGl/D//+eAwCV5sRMFAAzBRWPlvBYDRdQAg0XEAANG5ACDRvQAIgVNjUIG4gaz5cYAyY0DRZQAA0aEAINGpAADR7QAIgVRjcIGYgfZjlWNl/DH/+eAgPCqhRMFYANjnwUQDaITBWAMEapSTCFKGUsxoh1lEwWF8gqVA0QFAE1GKWUTBVWJCpWdZZOFlfKKlZfwx//ngAAhAlUjhYkIqWWThaWAipUpZhMGRokKltFGl/D//+eAIPq1sJMH8A4DRZQAA0aEAINEpAADRLQAIgVRjcIEYgRFjLNkpABjngQJIgfZjcII4gYz5RYByY0TBRAMY5W3CBbIAlWpZZOFpYCKlZfw///ngGDtDUVjjKoWE4XK8lnBRUVjhqoKHUXCRuOSqoBCVhMFkAxjaZYEk/U8ABMFAAy54ROFNgBxmbMF1UBj87QAEUWztrQAs4W0QBPXJQD9FqJV+Y5xFooGY4MG/BhBEQWYwZEFLtQy2HEW8Rbl+m/wD/sTBQAMI4R5AaOEqQACVallk4WlgIqVl/D//+eAoORv8O/4l/DH/+eAINgIEIVFQkamhpfw///ngGDwk3X1D/nxl/DH/+eAgNZv8E/2NxXLPwMkBbGBTIVKBUgmxEJGQlX5wHHBY4wKDCOlmQghajMFikAFCp1lk4WF8oqViMFjcEQRIkVjbZUPUlUztaQABgUzaKgAskYzh4YAswWWASllEwVFiTMGoQAdZRMFhfKzB6EAKBhCzJfwx//ngCDNA6ypCNJVHWYTBobyCpYQQqqKM4WFQSraMpRjhQoAIWVjH6QAY35ECQgQskUihpfw///ngGDWk3X1D7XpAUQhSrOEhEHinFJMQkZiSJG/CBDCRSaGl/D//+eAANSTdfUP45MF7m/wr+i3Fcs/I6iFsGPCCgSzNaAAY4oKADmotxXLPyOohbCFRWOZCgCZxRMFgAwjAKwAb/CP5WOKCuRjGAXkEwWQDCMArABv8E/kUkwhSlG9EwVwDCMArABv8C/jAABBEQbGlwAAAOeAoAG3RQ8Ak4UFJDNVtQKyQEEBgoAJyQVGYxzFADdlYgITBQWggoA3xYwBEwUFqIKAN0UPABMFBSQzhaUCgoBBEQbGlwAAAOeAYAKTFUUAwYExgTeGAGADIAYLQgVNjSMopgqyQEEBgoAJyQVGYxzFADe1xAQTBQVAgoA3ZWICEwUFoIKAN0UPABMFBSQzhaUCgoBBEQbGIsQmwgFFNyQMYJMEFQCT9fQPlcGXAAAA54DACEIFk1UFAROGZf0mhW3SigWilSOgBQAmhdm/NyU4QBMFBRAFBXMQVTA3Bcs/EwWFCpMFwAMFSLcmDGAFR5XN8ydA8aHjBEEDpMYLsxeYABPG9/9hjiOuxgpzJkDxHeKKBLaUI6TkDAOmhgtdjiOsxgrxFREFBQfh9bJAIkSSREEBgoAAABN19Q+TBaACY2S1ABMFoAKCgEERBsYixCbCSsATdfUPBUZByYVGBUU3hwBgOFcTeQcQk1SJANGMgeaZwIFEGagTBgAgmeATBgAQt4YAYLhWWY6w1jf2AWA0Vs4GY9IGAoMmBgiT9vYHk4YGCCMg1gg0VsIGY8YGAIMmBgiFiu3aNFY3lwCAfRf5jhMX1QCTlwUBXY/ZjjTWAyYGCIVGE3b2B2MF1QKJRmMX1QIhZc1GBag3hQBgKFV5gVXFBUZjAMUKCUZjE8UKBUapv81GRWUTBXWwMaDFRjcVAgATBQU0s5bVAFWON/QBYCMgxAg3Rg8AEwYGJDRUs4XFAjPVpQK3BQCA1Y0s1Jfwx//ngICRKFRCBWNIBQADJQQIBYlt2QFFKaA39QFgaFUdgbf1AWCwVQYGBYKw1beFAGCwVRN29u8zZiYBsNWJxLBVE3b237DVskAiRJJEAklBAYKAAUaBRglF0bWBRsG1AABBEQbGIsQ3hgBgNFYuhPF1k4X1AwXBE/X2+yjWSE5tjRNlBRRIzhMFIAOX8Mf/54CgiAmoE+UGBCjWSE5tjRNlBVBIzjeFAGAsVZP19fd9FBN2BAjRjSzVskAiREEBgoABEQbOIswmykrITsZSxBMF0AY1Ro1GDUeBRYFHlxDI/+eAAK8TBdAGNUaJRglHgUWBR5cQyP/ngKCtt4kAYE6ESEy3xf8Ak4X1A22NtwUAFJOFBVBNjUjMCFTBdZOF9Q9tjRMFBSAI1DdFDGCDJcUK+ZkjJrUKDEX5mQzFNzUAYIMlxQ35mSMutQw3JQBggyXFDfmZIy61DChUtwUA/P0VbY0o1AhA+XX9FW2NCMBIVLcFfP79FW2NtwWAAE2NSNQIQP11k4X1V22NCMBoWLcFAID9FW2NaNg3BQxgUEV5mlDFcFwTdvb9cNw3ZgJgEwcGCFRPqZpUz1RXk/YG+FTXcFy3BvD//RZ1jnDck4YJCJBC8Y2MwgxF3ZkMxYhC7XX9FW2NiMIjrAkCfVVowBMFEAaJRglHgUUBRoFHlxDI/+eAAJ0oVLcFACBNjSjUDUWX4Mf/54BAcDcFADwTCvXvzoWoVTN1RQGo1RMFwBKX4Mf/54BgbgOlyQoTFAUBQYCTBYT9meEFSTGgkwVk/jM5sAAGCZNVBQGhjZO1FQB9FRM15f9tjRHhBUmTBQBAAUWXAAAA54BgyaqESoWihZcAAADngGCzWcETFqUAE9XUAM4Ek1UWALOGtACztZYALpWTtRYAswW1QBOF9v+BRpcAAADngGA6LdW3hQBg6MU3NR2PEwWlEhOHBQhI0zcFsIQI0yOiBQo3RdhQEwUVqkjPUEO39v9//RZ1jlDDI64FCLf1AWDo0QFFqMXo0fJAYkTSREJJskkiSgVhgoAAAAAAAaCCgDclOEATBQXgcxBVMIKAtwgAYLcVyz+TgsWyEWYWlsVGqgYzh9IABUiDp8gBk/f3P43Lg6cIAANFhkAN6UhDFpUjAPUASEMFBbNH1QAMQ5O3FwD9F32NSMPjmKX8IwQGQeG3NwUAYIVFDMmCgAAAA0WUAINFhAADRqQAg0a0ACIFTY1CBuIGVY5RjYKCA0XUAINFxAADRuQAg0b0ACIFTY1CBuIGVY6CggNFFAGDRQQBA0YkAYNGNAEiBU2NQgbiBoKCKWUTBUWJCpWpZZOFxYKKlRMGgAWCgjNqpgADRZQAg0WEAANGpACDRrQAgoKTdgYCge4RyrMGwECzVtUAMxXFALOVxQDVjYKAsxXFAAFFgoCTdgYCge4RyjNVxQCzBsBAs5bVAFWNs9XFAIKAM9XFAIFFgoABRsFGM1fVAGNjtwA6hTM3twB9F3WPhYI6lu32MoWCgJcCAADngoIhOoq2iTKLroQqiRHHYwJLAzM1SwEFoGMFCwhjejsJToXahZcAAADngGD7XeH9Sm2gM7U0AYFGJeFjDwsEWoXShZcAAADngID5qooTdvUDToXShZcAAADngAAYgUYFRjMXVgEztqQAswe7QDOEx0BjTQQAiYxjBUQBszdEARmgs7c0AdmOge8iiwWBE5b1AVGNhYEFg/m3WoQ2iGmgAUhZoAFEAUiz1jQDs/Q0A6WoYxY7CQFEM9VkA7P0ZAOzNaAAfRWTBhUAE7UWADOIpQCZqJMFAAKziqVAE/b6A06F0oWXAAAA54AgD4FGBUYzFlYBM7ekALMHu0Azh+dAY0YHAImM0Y4JyzqLBYETl/UBWY2FgQWC8b8BRAFIM9U0A7P0NAPJjkqFBMUUwUDFIyIJAZcCAADngsIMgoCT1QkBM1g7AzN0OwOV6ZPVBAETFgQB0Y0z1jUDs/U1A8IEwYDCBcWNs9Y1A7P0NQOTFQYBQYIzaAYBzY4BRHW3YwVEAbM1RAEZoLO1NAGZwYFGYb+T1hkAfgqzZtoAk5f5ATcHAIAztfQAMwbUQAmOY0YGAJ2M2Y0JyjKEhYMTlfYByY+FggWD+b8BRLPWNAOz9DQDzY6RvxcDAABnAOPbFwMAAGcAo92XAgAA54IiBJcAAADngODgskUiRfJABWGCgPJAYkTSREJJskkiSpJKAksFYYKCAREGziLMJspKyE7GUsRWwlrAgoIBEQbONoeyhi6GqoUoAIKCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANXEGwJcAAACTgEA7dao1cQbAlwAAAJOA4D59ojVxBsCXAAAAk4BgfkWiNXEGwJcAAACTgEB+Sao1cQbAlwAAAJOAIH5RojVxBsCXAAAAk4AAfp2qNXEGwJcAAACTgOB9paI1cQbAlwAAAJOAwH2pqjVxBsCXAAAAk4CgfbGiNXEGwJcAAACTgIB9Pao1cQbAlwAAAJOAYH0FqjVxBsCXAAAAk4BAfQ2iNXEGwJcAAACTgCB9Eao1cQbAlwAAAJOAAH0ZojVxBsCXAAAAk4DgfOWoNXEGwJcAAACTgMB87aA1cQbAlwAAAJOAoLXxqDVxBsCXAAAAk4DAtPmgNXEGwJcAAACTgOCzwaA1cQbAlwAAAJOAALNNqDVxBsCXAAAAk4AgslWgNXEGwJcAAACTgECxWag1cQbAlwAAAJOAYLBhoDVxBsCXAAAAk4CAr62oNXEGwJcAAACTgKCutaA1cQbAlwAAAJOAwK25qDVxBsCXAAAAk4DgrIGoNXEGwJcAAACTgACsiaA1cQbAlwAAAJOAIKsVqDVxBsCXAAAAk4BAqh2gNXEGwJcAAACTgGCpIag1cQbAlwAAAJOAgKgpoJcAAACTgIAfFsIaxB7Gcsh2ynrMfs4q0C7SMtQ21jrYPtpC3EbeosCmwsrEzsbSyNbK2szezuLQ5tLq1O7WjtiS2nMjEDSa3nMjADAawXMjIDQaw3MjMDQaxQARotwKhXERBsDvAMAVgkAqwEgAgpACRe8AYBcRAXZTcxATNApDcxADMIJAkkIiQ7JDQk7STmJP8k8CVZJVIlayVkJX0ldiWPJYBkSWRCZJtklGStZKZkv2SwZcllwmXbZdxlFWUmZRcwAgMAEAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAb/Af0G/wv9Bv8F/Rb/D/0W/wn9Jv8D/Tb/Df02/wf9Rv8B/Vb/C/1W/wX9Zv8P/Wb/Cf12/wP9hv8N/Yb/B/2W/wH9pv8L/ab/Bf22/w/9tv8J/cb/A/3W/w391v8H/eb/Af32/wv99v8F/gb/D/4G/wn+Fv8D/ib/Df4m/wf+NzJSA0CgW3JQxgLpUDJoUMA6WFFL1GY3jWAAUGI6TFFKFFc6AFMIKAoUVzsAUwtyUMYCOkpRSCgPMlIDRjxgUAFwMAAGcAA4gAAPMlIDRjxgUAFwMAAGcA44YTlRUABYGxRWN7tQAKBbcFyz+ThUUOLpUcQZHDgocXAwAAZwBjhKqFBUUXAwAAZwCDAE1xEAIjJhYEIMZkwiMgJgUjLjYDIyxGAyMqVgMjKGYDIyZ2AyMkhgMjIpYDIyCmAyMutgFzJkDxLshjHQY4tyUMYAOpxQoDrQULcyZA8WMTBjgFRjMWpgAjoMUM8yVA8WOaBTZ9FSrEKAgTBgAQl+DH/+eA4CCBTYFLN7V8BxMLFVM3Bcs/EwYFCLcmDGBKwGrCWsYyyjNluQGzZX0BTY1jCwUYY5sNAjMFcEEz9asAMwVlA22BMpUDRQUAEwUFAmMJCQKzBSBBs3W5ALOFZQPtgbKVg8QFAA2oMwWwQTP1rQAzBWUDbYEylQNFBQDjGwn8swWgQbN1vQCzhWUD7YGylYPFBQCThAUCs2WpAZnhkwQFBBOVJABVjQhBE7wEBBOEBPyTBQAEs4qVQJO8FABRwQoFNpUDKoUMBUWBRSKGlwAAAOeAgKIqhK6JBUWBRVaGlwAAAOeA4KETBgAEY+PEACKFY+PEAM6FE3r6ABOE/P+zeaQAbYwFRYFFJoaXAAAA54CgnrMGgEEz9qYA7Y4SCigIKprShMhAjEDYRJxEVY3RjUGPs+c3AZzE2MSMwMjAsagFRYFFJoaXAAAA54DgmqqJLooFRYFFIoaXAAAA54DAmSqLLoQFRYFFVoaXAAAA54AgmRMGAARj48QAooVj48QAWoUzBoBBs3ZGATN2NgH9HDP0vACz+awAMksTRfT/k8X5/5PG9v8TRvb/M3nJADN93QCz/b0As/urAFJGtyYMYI21OUWiRWNttRgTlSUAtwXLP5OFhQoulQhBwUUSRmNxtRgSBSwILpVMQQhBAUkBS7P7xQCCRTN8tQA3tXwHkwwVUzcFyz8TDQUIkw0ABDNlLAGz5WsBTY1jCQUQYxsJAjMFYEEzdasAMwWVA22BapUDRQUAEwUFAmMJDAKzBYBBs3W8ALOFlQPtgeqVg8QFAA2oMwUgQTN1qQAzBZUDbYFqlQNFBQDjGwz8swVwQbP1uwCzhZUD7YHqlYPFBQCThAUCs2V8AZnhkwQFBCaFl/D//+eAYBGTFQUBwYGThWX9kcXCRZcAAADngOALBUWBRSaGlwAAAOeAIISqia6KE4YE/AVFgUWXAAAA54DggiqKLoQzhp1ABUWBRZcAAADngCCCY+O0AaKFY+O0AVKFE7YEBDMGwECzdlYBM3Y2ARO3FAB9F/mNeY0TRfX/k8X1/xNG9v+Txvb/s/vbADN8zAAze7sAM3mpAN21CAKDIMUEIEVkQQMpBQSDKcUDAyqFA4MqRQMDKwUDgyvFAgMshQKDLEUCAy0FAoMtxQFxYYKAAABCBUGBNyY4QBMGBmsKBTKVHEE3JThAEwXlo2OEpwAuhYKHAACqhQlFFwMAAGcAI8CqhQ1FFwMAAGcAY7+qhRFFFwMAAGcAo76qhRVFFwMAAGcA472qhRlFFwMAAGcAI72qhR1FFwMAAGcAY7yqhSFFFwMAAGcAo7uqhSVFFwMAAGcA47qqhSlFFwMAAGcAI7qqhS1FFwMAAGcAY7mqhTFFFwMAAGcAo7iqhTVFFwMAAGcA47eqhTlFFwMAAGcAI7eqhT1FFwMAAGcAY7YAAD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QD4aOEA+GjhAPho4QA==","text_start":1077411840,"data":"0As4QKIMOEDeDDhA0As4QIoOOECiDDhATA04QLQNOEDoDjhAFA84QOIUOEAyDThA4hQ4QOgNOEDQCzhAogw4QN4MOEBeDzhAgA04QI4MOED4EThAZBA4QCgNOECiDDhAAAEcAh0OGAMeFhQPGREECB8bDRcVExAHGgwSBgsFCgnA29zb3QAAAAEAAAACAAAAAwAAAAQAAAAFAAAABgAAAAcAAAAIAAAACQAAAAoAAAALAAAADAAAAA0AAAAOAAAADwAAAD4aOEA+GjhAAAAAAD4aOEA+GjhAPho4QAAAAAA+GjhAPho4QD4aOEAAAAAAPho4QAQAAAAEAAAABAAAAAQAAAAMAAAABAAAAAwAAAAEAAAABAAAAAwAAABAAAAAgAAAAAAIAAAAAAAQQAAAAAAgAAAEAAAABAAAACAAAAACAAAACAAAAAgAAAAIAAAACAAAABAAAAAIAAAAEAAAAAgAAAAIAAAAEAAAAL////9///////f//////++//////9////v////7////3/////3///8=","data_start":1070268448}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/2/esp32c3.json b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32c3.json
new file mode 100644
index 00000000..97538d65
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32c3.json
@@ -0,0 +1 @@
+{"entry":1077411840,"text":"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YwsFGGObDQIzBXBBM/WrADMFZQNtgTKVA0UFABMFBQJjCQkCswUgQbN1uQCzhWUD7YGylYPEBQANqDMFsEEz9a0AMwVlA22BMpUDRQUA4xsJ/LMFoEGzdb0As4VlA+2BspWDxQUAk4QFArNlqQGZ4ZMEBQQTlSQAVY0IQRO8BAQThAT8kwUABLOKlUCTvBQAUcEKBTaVAypFEQVFgUUihpcAAADngICiKoSuiQVFgUVWhpcAAADngOChEwYABGPjxAAihWPjxADOhRN6+gAThPz/s3mkAG2MBUWBRSaGlwAAAOeAoJ6zBoBBM/amAO2OEgooCCqa0oTIQIxA2EScRFWN0Y1Bj7PnNwGcxNjEjMDIwLGoBUWBRSaGlwAAAOeA4JqqiS6KBUWBRSKGlwAAAOeAwJkqiy6EBUWBRVaGlwAAAOeAIJkTBgAEY+PEAKKFY+PEAFqFMwaAQbN2RgEzdjYB/Rwz9LwAs/msADJLE0X0/5PF+f+Txvb/E0b2/zN5yQAzfd0As/29ALP7qwBSRrcmDGCNtTlFokVjbbUYE5UlALcFyT+ThYUKLpUIQcFFEkZjcbUYEgUsCC6VTEEIQQFJAUuz+8UAgkUzfLUAN7V8B5MMFVM3Bck/Ew0FCJMNAAQzZSwBs+VrAU2NYwkFEGMbCQIzBWBBM3WrADMFlQNtgWqVA0UFABMFBQJjCQwCswWAQbN1vACzhZUD7YHqlYPEBQANqDMFIEEzdakAMwWVA22BapUDRQUA4xsM/LMFcEGz9bsAs4WVA+2B6pWDxQUAk4QFArNlfAGZ4ZMEBQQmhZfw///ngKDokxUFAcGBk4Ul/JHFwkWXAAAA54DgCwVFgUUmhpcAAADngCCEqomuihOGBPwFRYFFlwAAAOeA4IIqii6EM4adQAVFgUWXAAAA54AggmPjtAGihWPjtAFShRO2BAQzBsBAs3ZWATN2NgETtxQAfRf5jXmNE0X1/5PF9f8TRvb/k8b2/7P72wAzfMwAM3u7ADN5qQDdtQgCgyDFBCBFZEEDKQUEgynFAwMqhQODKkUDAysFA4MrxQIDLIUCgyxFAgMtBQKDLcUBcWGCgAAAQgVBgTc2OEATBgarCgUylRxBNyU4QBMFBddjhKcALoWChwAAqoUJRRcDAABnACPAqoUNRRcDAABnAGO/qoURRRcDAABnAKO+qoUVRRcDAABnAOO9qoUZRRcDAABnACO9qoUdRRcDAABnAGO8qoUhRRcDAABnAKO7qoUlRRcDAABnAOO6qoUpRRcDAABnACO6qoUtRRcDAABnAGO5qoUxRRcDAABnAKO4qoU1RRcDAABnAOO3qoU5RRcDAABnACO3qoU9RRcDAABnAGO2AABwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QHAdOEBwHThAcB04QA==","text_start":1077411840,"data":"4Aw4QLYNOEDyDThA4Aw4QJwPOEC2DThAXg44QMYOOED6DzhAJhA4QJwKOEBEDjhAnAo4QPoOOEDgDDhAtg04QPINOEBwEDhAkg44QKINOEAKEzhAdhE4QDoOOEC2DThAAAEcAh0OGAMeFhQPGREECB8bDRcVExAHGgwSBgsFCgnA29zb3QAAAAEAAAACAAAAAwAAAAQAAAAFAAAABgAAAAcAAAAIAAAACQAAAAoAAAALAAAADAAAAA0AAAAOAAAADwAAAHAdOEBwHThAAAAAAHAdOEBwHThAcB04QAAAAABwHThAcB04QHAdOEAAAAAAcB04QA==","data_start":1070137376}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/2/esp32c6.json b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32c6.json
new file mode 100644
index 00000000..25c77106
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32c6.json
@@ -0,0 +1 @@
+{"entry":1082130432,"text":"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lwAAAOeAYJIihZcAAADngACUhUUihZcAAADngACUhUUihZcAAADngACUIoWBRZcAAADngECQIoWBRZcAAADngECQSkVFjSrJABECywLNAtG3BABAJs8ihZcAAADngOCShUUihZcAAADngKCNhUUihZcAAADngKChhUUihZcAAADngKCNClUzdSUBRY2q1CrRAtkIGYVFlwAAAOeAgI0IGZcAAADngKCNCBmXAAAA54DAjQgZgUWXAAAA54CAiAgZhUWXAAAA54CAnAgZhUWXAAAA54CAiALTIBEC1QLXSBGBRZcAAADngACLGlUzdXUB3t4q00gRlwAAAOeAgIFIEZcAAADngKCBIoWXAAAA54BAiSKFlwAAAOeAYIkihZcAAADngICJKlWDRcEKM3W1ATNlhQGq1irVLtdBRSrbSBmpRZfw///ngOBISBmtRZfw///ngCBIWlW3RY///RVtjbcFEABNjSrbSBn1RZfw///ngEBGAt0oGZcAAADngKCEKBmXAAAA54CghCgZlwAAAOeAoIQoGZcAAADngKCEKBmXAAAA54CghCgZlwAAAOeAoIQoGZcAAADngKCEKBmXAAAA54CghCgZlwAAAOeAoIRKXFpVqs5qVarMKkUq0DpFKtJaRSrYakUq1npFKtQaVSreAsVkAQLHAskoAYVFl/D//+eAQHAoAZfw///ngOBxKkUzdWUBKsUoAYVFl/D//+eAIHIoAYFFl/D//+eA4HIoAZfw///ngMBtKAGX8P//54DgbYVFJoWX8P//54CgboVFJoWX8P//54CgbiaFl/D//+eAwG4mhYFFl/D//+eAwG4mhYFFl/D//+eAwG5KRTN1NQEqyQQRAssCzQLPAtEmhZfw///ngEBvJoWBRZfw///ngABqhUUmhZfw///ngAB+hUUmhZfw///ngABqClVKxDN9JQFq0QLTJBEC1QLXSBGBRZfw///ngOBrGlUzdXUBKtNIEZfw///ngIBiSBGX8P//54CgYiaFl/D//+eAQGomhZfw///ngGBqJoWX8P//54CAaipVM3W1AYNFwQo3BgAIUY2qyirVLtcTBQAgKtkIGalFl/D//+eAoCkIGbFFl/D//+eA4CgIGbVFl/D//+eAIChKVbcFceD9FW2NtwWAEk2NKtkIGflFl/D//+eAQCYIGf1Fl/D//+eAgCUC20gZ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nMTYxIzAyMCxqAVFgUUmhpcAAADngGCNqokuigVFgUUihpcAAADngECMKosuhAVFgUVWhpcAAADngKCLEwYABGPjxACihWPjxABahTMGkEGzdkYBM3Y2Af0dM/S9ALP5rQBySxNF9P+Txfn/k8b2/xNG9v+z+8sAM3nZADN9vQAzfKwAEla5vUlF4kVjYLUaE5UlALdFgECThcUOLpUIQcFF0kZjdLUYEgUsEC6VDEVQQQhBAUkz+9UAskWze7YAwkUzfLUAN7V8B5MMFVM3RYBAEw1FDJMNAAQzZWwBs+UrAU2NYwkFEGMbCwIzBSBBM3WpADMFlQNtgWqVA0UFABMFBQJjCQwCswWAQbN1vACzhZUD7YHqlYPEBQANqDMFYEEzdasAMwWVA22BapUDRQUA4xsM/LMFcEGz9bsAs4WVA+2B6pWDxQUAk4QFArNlfAGZ4ZMEBQQmhZfg///ngGC3kxUFAcGBk4U1+5HFglWXAAAA54DgCwVFgUUmhpfw///ngIB2qomuihOGBPwFRYFFl/D//+eAQHUqii6EM4adQAVFgUWX8P//54CAdGPjtAGihWPjtAFShRO2BAQzBsBAs3ZWATN2NgETtxQAfRf5jXmNE0X1/5PF9f8TRvb/k8b2/7P72wAzfMwAM3m5ADN7qwDdtQgCgyDFBSBNZEkDKQUFgynFBAMqhQSDKkUEAysFBIMrxQMDLIUDgyxFAwMtBQODLcUCNWGCgAAAQgVBgTdGgEATBgbvCgUylRxBNzWAQBMFxQ1jhKcALoWChwAAqoUJRRcDAABnAGO/qoUNRRcDAABnAKO+qoURRRcDAABnAOO9qoUVRRcDAABnACO9qoUZRRcDAABnAGO8qoUdRRcDAABnAKO7qoUhRRcDAABnAOO6qoUlRRcDAABnACO6qoUpRRcDAABnAGO5qoUtRRcDAABnAKO4qoUxRRcDAABnAOO3qoU1RRcDAABnACO3qoU5RRcDAABnAGO2qoU9RRcDAABnAKO1qoVBRRcDAABnAOO0qoVFRRcDAABnACO0qoVJRRcDAABnAGOzqoVNRRcDAABnAKOyAADcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQNwwgEDcMIBA3DCAQA==","text_start":1082130432,"data":"4AuAQLYMgEDyDIBA4AuAQJwOgEC2DIBAXg2AQMYNgED6DoBAJg+AQPYUgEBEDYBA9hSAQPoNgEDgC4BAtgyAQPIMgEBwD4BAkg2AQKIMgEAKEoBAdhCAQDoNgEC2DIBA3////9/////f////f////+/////f////3//////+//8gAAAAIAAAACAAAACAAAAAEAAAACAAAAAgAAAAAAEAAAABHAIdDhgDHhYUDxkRBAgfGw0XFRMQBxoMEgYLBQoJwNvc290AAAABAAAAAgAAAAAAAAAAAAAAAwAAAAQAAAAAAAAAAAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAAQAAAAIAAAAFAAAABgAAAAkAAAAKAAAACwAAAAwAAAANAAAADgAAAA8AAAAQAAAAEQAAABIAAAATAAAA3DCAQNwwgEAAAAAA3DCAQNwwgEDcMIBAAAAAANwwgEDcMIBA3DCAQAAAAADcMIBA","data_start":1082146852}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/2/esp32h2.json b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32h2.json
new file mode 100644
index 00000000..310e523e
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32h2.json
@@ -0,0 +1 @@
+{"entry":1082130432,"text":"twCAQGeAgAAXNQAAEwUFtpf1AACThUX4Y1e1AIFGFMERBeNOtf4XBYAPEwWl/ZcFgA+ThSX9Y1e1AIFGFMERBeNOtf6BQAFBgUEBQoFCAUOBQwFEgUQBRYFFAUaBRgFHgUcBSIFIAUmBSQFKgUoBS4FLAUyBTAFNgU0BToFOAU+BT5cxAACTgWEu8yNA8bcCAACTggIAY/RyAG8AgAQX8QMAEwEh85FCMwFRQBNxAf8KhG8AQABBEQbGlxAAAOeAQGWXEAAA54DAVjcFgUC3xa3ek4XlqyMqtfiXAAAA54DAVW8AAABBEQbGlwCA/+eA4AUTNRUAfRUTdTUGskBBAYKAQREGxoNFFQADRgUAg0YlAANHNQCiBdGNwgZiB9mOM+i2AINFVQADRkUAg0ZlAANHdQCiBdGNwgZiB9mO1Y0DRpUAg0aFAANHpQCDR7UAIgZVjkIH4gddj1mOg0jVAANHxQCDR+UAg0b1AKIIM+foAMIH4gbdjtmOg0gVAYNCBQEDQyUBg0c1AaIIM+dYAEID4gez52cAXY+DQlUBg0hFAQNDZQEDRXUBogKz5xIBQgNiBTNlZQDJj0KFlwCA/+eAwPkTNRUAfRUTdUX8skBBAYKAsoYuhoFFFwMAAGcAAw5BEQbGIsQmwi6EqoSXAAAA54DAKilGJoWihZcAAADngAArA0W0AINFpAADRsQAg0bUACIFTY1CBuIGs+XGAMmNA0X0AANG5ACDRgQBA0cUASIFUY3CBmIHM2bXAEmOJoWXAAAA54CgJiaFskAiRJJEQQEXAwAAZwBjJAERBs4izCbKSshOxjaJsokuhKqElwAAAOeAoCIhRiaFooWXAAAA54DgIiaFzoVKhpcAAADngAAikwWEAAlGJoWXAAAA54AAISaF8kBiRNJEQkmySQVhFwMAAGcAgx45cQbeItwm2krYTtZS1FbSWtBezmLMZspqyG7GqooDK0UBREU2ijKJronajGNj2wDSjAOkCgGDrYoAJsTmlMFrBW1jf5QCExUEAQntMwx0AWPqjQETVQQBlwCA/+eA4OIZyWGgE1XEAJcAgP/ngKDhLe0zDKQBI6iKAWKE42Wc/AFEoktejGOFDAQFZeaEY+OsAIVkY2yKBrMFiQBihSaGY4cJAJcAgP/ngODaKaCXAID/54DA3RM1FQB9FRN1RQyjjqoAs4ycQCaUJpzjnwz6AUWzhYsA1obMxrMFi0AzNrsAfRbxjczKGaATBWAD8lBiVNJUQlmyWSJakloCW/JLYkzSTEJNsk0hYYKAAAABEQbOIswmykrITsayiS6JKoQihZcAAADngAAUE3X1DxMFBfR99YFEIoWXAAAA54CgEpN19Q8ThlXyHeIihZcAAADngGARE3X1D5MFRfKZzRMFNfJx+RMFsA1j7TQBJaiThQX0kc1j5zQBNaATBQAMY/M0A7MFmQAjgKUAhQRtt2PrmQBKhaaF8kBiRNJEQkmySQVhgoAAAEERBsYixCbCSsAyia6EKoSXAAAA54CgAiKFpoVKhpcAAADngOACIoWyQCJEkkQCSUEBFwMAAGcAgwC3NYBAk4WFqgVGFwMAAGcAwxIBEQbOIswmykrITsZSxDKEroQqiTc1gECTCZWqNzWAQBMKtaoVzgPFBACTBQX0gcmTBVXyiekJRkqF0oUZqAlGSoXOhTmgowOhAJMFcQAFRkqFlwAAAOeAIA2FBH0UYfTyQGJE0kRCSbJJIkoFYYKAQREGxiLEOcmFRWMUtQo3NYBAEwWFuMVFqgUulaFFgUfzdwQwEEFUQQNHhQChi5nDc6AFMDWOWY4TNhYAbfIBRHN0BDCXEAAA54BgaAWJNcEhiDnIIUVzIAUwuaA3NYBAEwWFuMVFqgUulaFFgUfzdwQwEEFUQQNHhQChi5nDc6AFMDWOWY4TNhYAbfIBRHN0BDCXEAAA54CAYwWJGckhiAHEIUVzIAUwLoWyQCJEQQGCgAAAMcGFRmMQ1QY39QBgDUgVypMGAASyh2Nk1gCTBwAEs4j1AD6HmceDxgUAhQUUwX0XffsjIgUBTEGNifXdHY7GhWH6goAuljcFAGDjjMX+VE2iBuPOBv6DxgUAhQUUweOYxf7FtwAAEXGG36LdptvK2c7X0tXW09rR3s/izebL6snuxyllEwUFgjMBoUABRXN1BDC3NYBAA8altuMUBmYFRiGJI4XFtgHFIUVzIAUwNwULYJMGBQjsRjcGAHDRjezG7EY3BgAC0Y3sxjfVCmBMRZPlRQBMxQxFk+UVAAzFN+QKYBMGBIAITm2ZCM4ITqllk4WlgLMJsQCNZZOFhbIzCrEAE2WFAAjOEwVgBolFDUaBRgFHlxAAAOeAAN8TBWAGjUUVRgVHgUaXEAAA54DA3RMFYAaVRRVGkUYNR5cQAADngIDcEwVgBpVFHUaZRgVHlxAAAOeAQNsDJYSBHgXjXQX+N+UKYBMGBYAMTpPlRQAMzgxO3ZkMzjdkCWATBgQQSEoTdQXwSMoIThN1BfAJBQjOSEa3Bf3//RVtjcFlTY1IxpcQAADngMDnEwUABpcAgP/ngKCMNxULYAMgRQG3JYQek4VF6EzJlwCA/+eAwJEDRYUBjUVjHrUGkwUECMhFE2UVAMjFyEV1mcjFN/QAYIFFDMghRUjIDMgRRUjINzWAQBMFRY63JYBAk4VluyMqtQgTBVAClxAAAOeAIOIISBNlRQAIyDc1gECTCoW2E/XK/5P1OgCOBQVJMxa5AC8lxUQzVbUAE3X1D+McBUqFCmmiNwUAYAxRNwYQANGNDNEMQJPlFQAMwAxA9ZkMwLcFBACThRURDMkBRlDFnWWJBQzJUMUMQPGZhQUMwExANwaA//GNNwZwANGNTMCTBVARTMmDJYUJk+UVACMstQiDJYUJhYnt/TcEAGAIUBNlxQAI0AhQdZkI0AhQE3X1/EEFCNA3NYBAEwVFjrclgECThcWzIyK1CBMFEAKXEAAA54Dg1AhQtwUEAE2NCNAIULcFgABNjQjQAyWECRNlFQAjLKQIAyWECQWJbf03BQBgDFE3BoD/fRbxjZcSAADngoI5gyWFCYWJ7f03BQBgDFE3BkAA0Y2XEgAA54LCN4MlhQmFie39NwUAYAxRNwbA/30W8Y2XEgAA54LiNYMlhQmFie39PUWX8H//54DAbzcFAGBMRZPlFQBMxTc1gECTCgW3k4VKARP2xf+NiY4FhUazlrYALybWRLNVtgCT9fUP45oFNIlFIxi1thOFKgCNZZOFhbKKlTlGlwCA/+eA4LABSSOICgApRSOJqgANZRMFhbKzBKEADUQFZRMLBdoZyCaFgUVahpcAgP/ngMCt2pR9FH30jWsThAuOHWUTBYXyCpWNZZOFhbKKlSKGlwCA/+eAoKs3RQBgCF1xiWEVEzUVAIFFl/B//+eAIH0jIAoANwUAAdKFyMFBayMkagEFZcjFEwUAEIjJEwX7/8jJDWUTBYWyCpWX8P//54DgbErOVtBIEJMEoQcTBmAFgUWXAID/54BApR1lEwWF8rMFoQAmhSKGlwCA/+eAIKQTheuTbAgulRMG0ByBRZcAgP/ngICiEwXwBCMAqgATBYAEowCqABMFEAQjAaoAEwWQBKMBqgANZRMFhbKzBaEAEUZKhZcAAADngCCbRUkqCQ1lEwWFsgqVgUVKhpcAgP/ngKCdoUsTCwvwBU0JSjc1gEATBUWjKsw3dYBAEwVF+SrGNzWAQJMNhZ4JZRMFBXEqwhlMckWNZZOFhbKKlUqGlwAAAOeA4Ivj4HUdKoSujANFFQCDRQQAA0YkAINGNAAiBU2NQgbiBlWOs2SmADP1ZAETBQXw4wkFGINFRAADR1QAg0hkAINGdAAT1YQAk5eFAV2NI4CpAaOAqQCjgQkAI4FJAaODCQAjgwkAo4IJACOCCQAjhAkAo4QJAKOGCQAjhgkAo4UJACOFqQGjiAkAI4gJAKOHCQCTevUPE4Xq/yOHCQBJRmNgpgwKBW6VCEEChRMFAAzhRWPqvHiXEgAA54KiCbPlxgDJjQNFVAEDRkQBg0ZkAQNHdAEiBVGNwgZiB9mOVY0DRtQAg0bEAANH5ACDR/QAIgZVjkIH4gddj1mOg0aUAANHhACDR6QAg0S0AKIG2Y7CB+IExY/djn13aY861irUNpUq0jMFtgIq2jbYIx4BAiMdAQIjDKEDQUVjhKoAY5hKcRMFIAwRZmNvtm6X8H//54AAT6qFEwVQDGOWBW7FrROFCvMKBWJGMpUIQQKFl/B//+eA4EmqhRMFQAxjlQVs+aVhRWPvrGomhWOOqmsDRYEDYw4FWBOIjP6TAoQBYwMIXrMElAGTB/AOFoUDRgUABQWxj+Mclf6T9/cP8aMTBQAMY4J8aYNFhAAmhWONRWcDRYEDYw0FVEJWEwWADGMUBmYjDAECY5QFZnJFqWWThaWAipWX8P//54DgTRJFl/B//+eAIDOX8H//54CgN4GlLUVjeJV7lxIAAOeCAvCBRZfwf//ngEBKHaWBRIVFhYhjnwRgY1S8AB1EGaAThBUAckWTpXUAk8QVAKllk4WlgIqVl/D//+eAAEiihcm/HWUTBYXyCpVRRoFFl/B//+eAYHIdZRMFhfIzBqEAAUWBRZfwf//ngMAyYwoFShMFMAxdqxMFAAzhRWPnvFqXEgAA54KC6VGNg0WUAANGhACDRqQAA0e0AKIF0Y3CBmIH2Y7VjYjBaaMTBQAMwUVj7bxWA0WUAINFhAADRqQAIgXJjUIGg0a0AANKxACDRNQAg0vkAINK9AByReIGVY4zZLYAqWWThaWAipWX8P//54CgPBFlEwWFqZfwf//ngKAhNwUAYEhJYw8EaKIEs+VEAcIL4goz5noB0Y0TFsUAIYIiBXGBUY0zBbUCs1WFAgFFl/B//+eA4CETBYA+l/B//+eAYB2hSwlKYbETBQAMwUVj7LxMwlUTBYAMY5cFTJcSAADngqLZYxUFTANF1ACDRcQAA0bkACIFg0b0AMmNQgZyReIGVY4zZLYAqWWThaWAipWX8P//54AgMhJFl/B//+eAYBcClGmhLUVjfZVflxIAAOeCotQIQSOCqQCTVYUBo4O5AJNVBQEjg7kAIYGjgqkAuakTBQACY+asRJMFJAB5Rh1lEwWl8gqVl/B//+eAwFgdZRMFhfIKlSMAlQCtRaMAtQAdZRMFBfMKlZfw///ngIAck3X1D2OYBUARqRMFAAzhRWPivECXEgAA54LizpcSAADngiLUIgVNjUIG4gZVjjNkpgAdZRMFhfIKlQVmgUWX8H//54AAUillEwXFggqVl/B//+eAgHhjCAoEHWUTBYXyswWhAAVmIoWX8P//54DAE5N19Q9jlgVQBWXShGNjqgCFZCllEwXFggqVnWWThYXyipUmhpfwf//ngGB0MwqaQCaU4xwK+pcSAADngiLJl/B//+eAQEspZRMFRYgKlallk4VFiYqVl/B//+eAgHEDxKkHPUYpZRMF1YEKlallk4VViIqVl/B//+eA4EdyRSOJiQCpZZOFpYCKlSlmEwbGgQqWwUaX8P//54BgIQlKfbRyRallk4WlgIqVl/D//+eAYBgTBQAM4UVj5rwulxIAAOeCYr0zZ6YAlxIAAOeCYr5VjrNqpgADRVQBg0VEAQNGZAGDRnQBIgVNjUIG4gZVjpcSAADnguK/OoQiBU2NQgbiBlWOs2ymAB1lEwWF8gqVBWaBRZfwf//ngKA9I6kJACllEwXFggqVl/B//+eA4GOBRIFLMwVaAyrKIopjfYAKUkWqm2MLCgZj+XQHIoxWhGPjSgFShAVlBQVjc6Q8HWUTBYXyswWhAGaFIoaX8P//54CA/JN19Q9jnQUQckWdZZOFhfKKlSKGl/D//+eAYDIpZRMFxYIKlZ1lk4WF8oqVIoaX8H//54CAXDMKikCinKKUYoQZTOMZCvhyRallk4XFgYqVEUaX8P//54AAJQ1GY3i2NINFFQADRgUAg0YlAANFNQCiBdGNwgZiBVWNs2u1AOPni/SXEgAA54IirZfwf//ngEAvKWUTBUWICpWpZZOFRYmKlZfwf//ngIBVckWpZZOFRYiKlUFGl/D//+eAQCh5sRMFAAzBRWPlvBYDRdQAg0XEAANG5ACDRvQAIgVNjUIG4gaz5cYAyY0DRZQAA0aEAINGpAADR7QAIgVRjcIGYgfZjlWNl/B//+eAYPKqhRMFYANjnwUQDaITBWAMEaqhSwlKGUwxoh1lEwWF8gqVA0QFAE1GKWUTBVWJCpWdZZOFlfKKlZfwf//ngKAjckUjhYkIqWWThaWAipUpZhMGRokKltFGl/D//+eAIP21sJMH8A4DRZQAA0aEAINEpAADRLQAIgVRjcIEYgRFjLNkpABjngQJIgfZjcII4gYz5RYByY0TBRAMY5W3CBbIckWpZZOFpYCKlZfw///ngGDwDUVjjaoWE4XK8lnBRUVjhqoKHUXCRuOSqoBCVhMFkAxjaZYEk/U8ABMFAAy54ROFNgBxmbMF1UBj87QAEUWztrQAs4W0QBPXJQD9FqJV+Y5xFooGY4MG/BhBEQWYwZEFLtQy2HEW8Rbl+m/wD/sTBQAMI4SpAaOEqQByRallk4WlgIqVl/D//+eAoOdv8O/4l/B//+eAANpoCIVFQkamhpfw///ngGDzk3X1D/nxl/B//+eAYNhv8E/2NzWAQAMkxbaBTIVKBUgmxEJGQlXhxHnBY40KDCOlmQghajMFikAFCp1lk4WF8oqViMFjcUQRIkVjbpUPUlUztaQABgUzaKgAskYzh4YAswWWASllEwVFiTMGoQAdZRMFhfKzB6EAEwWhA0LKl/B//+eA4M6Dq6kI0lUdZhMGhvIKlhBCqoozhXVBKtoylGOFCgAhZWMfpABjfkQJaAiyRSKGl/D//+eAQNmTdfUPtekBRAlKs4R0Qd6coUtCRlJIib9oCMJFJoaX8P//54Dg1pN19Q/jkgXub/CP6Lc1gEAjpoW2Y8IKBLM1oABjigoAOai3NYBAI6aFtoVFY5kKAJnFEwWADKMMoQJv8G/lY4kK5GMXBeQTBZAMowyhAm/wL+ShSwlKSb0TBXAMowyhAm/wD+MAALMI1kCT9/gPIUhj9wcRN9gKYIMniACT50cAIyT4ADfjCmADKEODtwcAELNn+AAjKvOCAygDgrcHAP+zd/gAEwil+ZNy+A+dQyMg84Jj6VMCkweQCbPXBwGFi5XDtzeAQJODh6STlyIAnpcDrgcAt+MKYIOnA4Kz58cBI6DzghN19Q+T9fUPogWz46UAIyBzgIMlA4CaBePNBf635Qpgg6cFgMGDfVMzFdMAE0X1/wUGMxbDAFGNfY2FCDMWEwETRvb/eY4zFtYAUY0TdfUPQgU3BgABM+bDAFGNI6ClgAOlBYAaBeNNBf4dRWNmVQITBZAJM1UFAQWJGc03NYBAEwWFpooCFpUIQbflCmADpgWCcY0joKWCgoAAADdlCWCDJYUUk+UVACMktRSDJYUUhYnt/YKA8yVA8YXhQgVBgQoFtwUBYC6VhUUMwTcVACAMQZPlJQAMwYKAAABBEQbGIsQmwkrAAUU3CQFg/UQTBBUAk3X0D43BlwAAAOeAgAhCBZNVBQEThvX7IoVt0ooFypWEwSKF4b83JYBAEwUFMAUFcxBVMDc1gEATBcWvkwXAAwVItxYAIAVHjcnzJ0Dxne8EQcBCsxeYABPG9/9hjtDCcyZA8QXmigS2lJjIkEJdjpDC8RURBQUH6fl9VXMQRTCyQCJEkkQCSUEBgoAAABN19Q+TBRAEY2S1ABMFEASCgDcVC2AISZMVBQHBgRNWBQEtjhM2FgB9FRM15f9xjROGBf4zNsAAcY2CgF1xhsaixKbCysBO3lLcVtpa2F7WYtRm0mrQbs4TBdAGoUUBRoFGAUeXAAAA54Dg2RMF0AahRQVGhUYBR5cAAADngKDYEwXQBqFFCUaJRgFHlwAAAOeAYNcTBdAGoUUNRo1GAUeXAAAA54Ag1hMF0AahRRFGkUYBR5cAAADngODUEwXQBqFFFUaVRgFHlwAAAOeAoNMTBdAGvUUJRolGAUeXAAAA54Bg0rcMC2AjqgwOI6wMDiOuDA4joAwQI6IMECOkDBDmhohWtwUACP0VbY03BgDIUY2I1gOlzAltjbcFANBNjSOurAgThgwQSFoiBSGBtwUAD02NSNpIXkIFQYG3BaQGTY1I3pcAAADngODsYxQFMLcKAPADpQxA/RpWzLcLAID9G02ZI6CsQA1Fl/B//+eAQIC3lABgaXV9FSrKfXV9FSrINwUBgH0VKsYhZRMFJaEqwgllEwWV1irEN0UPABMFBSQqwDdkCWC3CgAgA6UMQHGZI6CsQAOlDELiRW2NI6CsQpcAAADngADlg6YMQAOmDEAT/TYAkwb9/6qNLooJRWPkpgATfTYAYwUNAgVFYwutAAlFYxKtAoMlBBPxmQVFBUsVoIMlBBMBRQFL8ZmFBRmoAUsFRRmoAyUEEwFLcZmTBSUABUUjKLQSCU2DpQxCjgX9gbNptQBjmQkAA6UMQrcFABBNjSOgrEKThgxAiFK3BQAQTY2I0hOHDAhIV02NSNdIVzcGACBRjUjXA6DMCgOgDEJIV4xSM3zFADcFAEAz+aUAYwkLAmMeDAADpcwKM2VVASOmrAoTBcASl+B//+eAIG1jGQkAA6UMQrcFAEBNjSOgrEKoVE4FY1IFAgOlBAgTdfUHEwUFCCOgpAioVEIFY0YFAAOlBAgFiW3ZqFTSRW2NkxXdAE2NqNSoVMJFbY2o1KhUskVtjbcFAARNjajUA6UECLcFAAhjBA0AtwUAIBN19QdNjSOgpAgiRWMDDQASRaxUs/V1AazUrFQ3BgCA0Y2s1Jfgf//ngIBjqFRCBWNIBQADpQQIBYlt2QFEGaDgVB2AqFQzdXUBqNRjmAkAA6UMQuJFbY0joKxCYwkLAmMODAADpcwKM2VVASOmrAoTBcASl+B//+eAgF5jCQkAA6UMQrcFAEBNjSOgrEJjlQ0AEwYAAjGggkUzBboCM1a1AkXCKgYTVdQATgSTVRYALpSzNbQALpWTNRQAswW1QBMF9P+BRpcAAADngOBFN2QJYOMEBdy3FQtgA6BFAMjBN0XYUBMFFaq3JQtgE4cFwEjTNwbEEhDTI6IFwkjPEEO39v9//RZ1jhDDI64FwLeVAGDo0QFGsMXw0belAGDo0bDF8NG2QCZElkQGSfJZYlrSWkJbslsiXJJcAl3yTWFhgoAAAAAAAaCCgDclgEATBQUAcxBVMIKAQREGxiLENwQAYEhME3X1DwnJCECXAAAA54DgAQWJddUAADcFAGCFRQzJskAiREEBgoC3NYBAE4aFuJFlspWDxYVAjenFRqoGMwfWAFxDPpYjAKYASEMFBamOEEOTthYA/RZ1jUjDYxemADd1gEAFRiMIxfgzNbAAgoBBEQbGIsQ39ABgSEARiQnJCECXAAAA54Bg+gWJfdUAADf1AGCRRUzJskAiREEBgoA3NYBAEwiFuMVHqgczB/gAFENIQ4NFhwA1jTNmtQAzNcAAEc4jBAcAk4UWAK2PE7YXAH0W8Y0Mw8KWg8UGAIKAA0WUAINFhAADRqQAg0a0ACIFTY1CBuIGVY5RjYKCA0XUAINFxAADRuQAg0b0ACIFTY1CBuIGVY6CggNFFAGDRQQBA0YkAYNGNAEiBU2NQgbiBoKCKWUTBUWJCpWpZZOFxYKKlRMGgAWCggzRgyWFCZPlFQAjLLUIgoIzaqYAA0WUAINFhAADRqQAg0a0AIKCk3YGAoHuEcqzBsBAs1bVADMVxQCzlcUA1Y2CgLMVxQABRYKAk3YGAoHuEcozVcUAswbAQLOW1QBVjbPVxQCCgDPVxQCBRYKAAUbBRjNX1QBjY7cAOoUzN7cAfRd1j4WCOpbt9jKFgoCXAgAA54KCITqKtokyi66EKokRx2MCSwMzNUsBBaBjBQsIY3o7CU6F2oWXAAAA54Bg+13h/UptoDO1NAGBRiXhYw8LBFqF0oWXAAAA54CA+aqKE3b1A06F0oWXAAAA54AAGIFGBUYzF1YBM7akALMHu0AzhMdAY00EAImMYwVEAbM3RAEZoLO3NAHZjoHvIosFgROW9QFRjYWBBYP5t1qENohpoAFIWaABRAFIs9Y0A7P0NAOlqGMWOwkBRDPVZAOz9GQDszWgAH0VkwYVABO1FgAziKUAmaiTBQACs4qlQBP2+gNOhdKFlwAAAOeAIA+BRgVGMxZWATO3pACzB7tAM4fnQGNGBwCJjNGOCcs6iwWBE5f1AVmNhYEFgvG/AUQBSDPVNAOz9DQDyY5KhQTFFMFAxSMiCQGXAgAA54LCDIKAk9UJATNYOwMzdDsDlemT1QQBExYEAdGNM9Y1A7P1NQPCBMGAwgXFjbPWNQOz9DUDkxUGAUGCM2gGAc2OAUR1t2MFRAGzNUQBGaCztTQBmcGBRmG/k9YZAH4Ks2baAJOX+QE3BwCAM7X0ADMG1EAJjmNGBgCdjNmNCcoyhIWDE5X2AcmPhYIFg/m/AUSz1jQDs/Q0A82Okb8XAwAAZwDj2xcDAABnAKPdlwIAAOeCIgSXAAAA54Dg4LJFIkXyQAVhgoDyQGJE0kRCSbJJIkqSSgJLBWGCggERBs4izCbKSshOxlLEVsJawIKCAREGzjaHsoYuhqqFKACCggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1cQbAlwAAAJOAYDt1qjVxBsCXAAAAk4AgP32iNXEGwJcAAACTgMB+RaI1cQbAlwAAAJOAoH5JqjVxBsCXAAAAk4CAflGiNXEGwJcAAACTgGB+nao1cQbAlwAAAJOAQH6lojVxBsCXAAAAk4AgfqmqNXEGwJcAAACTgAB+saI1cQbAlwAAAJOA4H09qjVxBsCXAAAAk4DAfQWqNXEGwJcAAACTgKB9DaI1cQbAlwAAAJOAgH0RqjVxBsCXAAAAk4BgfRmiNXEGwJcAAACTgEB95ag1cQbAlwAAAJOAIH3toDVxBsCXAAAAk4AAffGoNXEGwJcAAACTgOB8+aA1cQbAlwAAAJOAwHzBoDVxBsCXAAAAk4CgfE2oNXEGwJcAAACTgMCgVaA1cQbAlwAAAJOA4J9ZqDVxBsCXAAAAk4AAn2GgNXEGwJcAAACTgCCerag1cQbAlwAAAJOAQJ21oDVxBsCXAAAAk4BgnLmoNXEGwJcAAACTgICbgag1cQbAlwAAAJOAoJqJoDVxBsCXAAAAk4DAmRWoNXEGwJcAAACTgOCYHaA1cQbAlwAAAJOAAJghqDVxBsCXAAAAk4AglymglwAAAJOAoB8WwhrEHsZyyHbKesx+zirQLtIy1DbWOtg+2kLcRt6iwKbCysTOxtLI1srazN7O4tDm0urU7taO2JLacyMQNJrecyMAMBrBcyMgNBrDcyMwNBrFABGi3AqFcREGwO8AwBWCQCrASACCkAJF7wCAFxEBdlNzEBM0CkNzEAMwgkCSQiJDskNCTtJOYk/yTwJVklUiVrJWQlfSV2JY8lgGRJZEJkm2SUZK1kpmS/ZLBlyWXCZdtl3GUVZSZlFzACAwAQATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAABv8B/Qb/C/0G/wX9Fv8P/Rb/Cf0m/wP9Nv8N/Tb/B/1G/wH9Vv8L/Vb/Bf1m/w/9Zv8J/Xb/A/2G/w39hv8H/Zb/Af2m/wv9pv8F/bb/D/22/wn9xv8D/db/Df3W/wf95v8B/fb/C/32/wX+Bv8P/gb/Cf4W/wP+Jv8N/ib/B/43MlIDQKBbcVACAulRBJA6UFCb1GY3jWAAUGI6jFCKFFc6AFMBN19Q+CgKFFc7AFMLcVACAjqKUIgoDzJSA0Y8YFABfz//9nAIN2AADzJSA0Y8YFABfz//9nAGN1E5UVAAWBsUVje7UACgW3NYBAk4WFsy6VHEGRw4KHF/P//2cA43IAAKqFBUUXAwAAZwCDAE1xEAIjJhYEIMZkwiMgJgUjLjYDIyxGAyMqVgMjKGYDIyZ2AyMkhgMjIpYDIyCmAyMutgFzJkDxLshjHgY4twUBYIOsRRCDrYUQ8yVA8WOUBTiFRbOVpQA3FgAgDMbzJUDxY5oFNn0VKsQoCBMGABCX4H//54AAAoFLAUw3tXwHEwsVUzc1gEATBoWoZsJuwFrGMsoz5XwBs+WNAU2NYw0FGGObCwIzBYBBM3WsADMFZQNtgTKVA0UFABMFBQJjiQwCswWQQbP1vACzhWUD7YGylYPEBQANqDMFcEEz9asAMwVlA22BMpUDRQUA45sM/LMFsEGz9b0As4VlA+2BspWDxQUAk4QFArPlvAGZ4ZMEBQQTlSQAtwUBYE2NCEETvQQEE4QE/JMFAASzipVAE7kUAEHFCgW3FQAgLpUDKgUBBUWBRSKGlwAAAOeAIJoqhK6JBUWBRVaGlwAAAOeAgJkTBgAEY+PEACKFY+PEAM6Fk3n6ABME+f8zeaQAbYwFRYFFJoaXAAAA54BAlrMGoEEz9qYA7Y6SCSgIqpnOhMhAjEDYRJxEVY3RjUGPs+cnAZzE2MSMwMjAsagFRYFFJoaXAAAA54CAkqqJLooFRYFFIoaXAAAA54BgkSqLLoQFRYFFVoaXAAAA54DAkBMGAARj48QAooVj48QAWoUzBqBBs3ZGATN2NgF9GTN0uQAzeakAMksTRfT/k0X5/5PG9v8TRvb/s/zMALP93QCz+7sAM3ysAFJGub1JRaJFY221GBOVJQC3NYBAk4UFqy6VCEHBRRJGgkZjcLUYEgUsCC6VTEEIQQFJAUuz+9UAM3zFADe1fAeTDBVTNzWAQBMNhaiTDQAEM2UsAbPlawFNjWMJBRBjGwkCMwVgQTN1qwAzBZUDbYFqlQNFBQATBQUCYwkMArMFgEGzdbwAs4WVA+2B6pWDxAUADagzBSBBM3WpADMFlQNtgWqVA0UFAOMbDPyzBXBBs/W7ALOFlQPtgeqVg8UFAJOEBQKzZXwBmeGTBAUEJoWX8P//54Dg+5MVBQHBgZOF9fuRxcJFlwAAAOeA4AsFRYFFJoaX8P//54AAfKqJrooThgT8BUWBRZfw///ngMB6KoouhDOGnUAFRYFFl/D//+eAAHpj47QBooVj47QBUoUTtgQEMwbAQLN2VgEzdjYBE7cUAH0X+Y15jRNF9f+TxfX/E0b2/5PG9v+z+9sAM3zMADN7uwAzeakA3bUIAoMgxQQgRWRBAykFBIMpxQMDKoUDgypFAwMrBQODK8UCAyyFAoMsRQIDLQUCgy3FAXFhgoAAAEIFQYE3NoBAEwZGjgoFMpUcQTclgEATBYWyY4SnAC6FgocAAKqFCUUXAwAAZwADwKqFDUUXAwAAZwBDv6qFEUUXAwAAZwCDvqqFFUUXAwAAZwDDvaqFGUUXAwAAZwADvaqFHUUXAwAAZwBDvKqFIUUXAwAAZwCDu6qFJUUXAwAAZwDDuqqFKUUXAwAAZwADuqqFLUUXAwAAZwBDuaqFMUUXAwAAZwCDuKqFNUUXAwAAZwDDt6qFOUUXAwAAZwADt6qFPUUXAwAAZwBDtqqFQUUXAwAAZwCDtaqFRUUXAwAAZwDDtKqFSUUXAwAAZwADtKqFTUUXAwAAZwBDsygbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BAKBuAQCgbgEAoG4BA","text_start":1082130432,"data":"vAuAQJIMgEDODIBAvAuAQHgOgECSDIBAOg2AQKINgEDWDoBAAg+AQNIUgEAgDYBA0hSAQNYNgEC8C4BAkgyAQM4MgEBMD4BAbg2AQH4MgEDmEYBAUhCAQBYNgECSDIBAAAIAAAACAAAAAgAAAAgAAAABAAAAAgAAAAIAAAAQAAD//f////3////9////9/////7////9/////f///+///wABHAIdDhgDHhYUDxkRBAgfGw0XFRMQBxoMEgYLBQoJwNvc290AAAABAAAAAgAAAAAAAAAAAAAAAwAAAAQAAAAAAAAAAAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAAQAAAAIAAAAFAAAABgAAAAkAAAAKAAAACwAAAAwAAAANAAAADgAAAA8AAAAQAAAAEQAAABIAAAATAAAAKBuAQCgbgEAAAAAAKBuAQCgbgEAoG4BAAAAAACgbgEAoG4BAKBuAQAAAAAAoG4BA","data_start":1082141160}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/2/esp32s2.json b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32s2.json
new file mode 100644
index 00000000..23ffd983
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32s2.json
@@ -0,0 +1 @@
+{"entry":1073913140,"text":"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5hMQIABioAUQcSCV0P8F3P8CIQEAxRMCIQAAtRMCIQISIQMQIAAQNQDwQQAQASAS0f8CYQMA0UkAxgMCYQEAtgMCYQAAwUkA1gMCYQJFz/8BR/4A5hMQIABioAYQcSBVzP/F1v8CIQEAxhMCIQAAthMCIQISIQMQIAAQNgDwQQAQASAS0f8CYQMA0UkAxwMCYQEAtwMCYQAAwUkA1wMCYQIFyv8BM/4A5hMQIABioAcQcSAVyP+F0f8CIQEAxxMCIQAAtxMCIQISIQMQIAAQNwDwQQA2QQCBEP6tAr0D4AgAHfAANkEA8EEAAAAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQAAAAAAAAAAAAAAAAAAAAAAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQAAAAAAEowJABKMCQASjAkAEowJABKMCQASjAkAEowJABKMCQASjAkAAAAAABKMCQASjAkAEowJABKMCQASjAkAAAAAABKMCQASjAkAEowJABKMCQA==","text_start":1073905664,"data":"a5ACQG+RAkAbkgJAa5ACQGuSAkBvkQJA2pICQBCTAkBgkwJAvpMCQMCaAkDtkwJAwJoCQByUAkBrkAJAb5ECQBuSAkDGlAJA0pUCQF2RAkAClgJAWJYCQFmYAkBvkQJAvKYCQCKkAkADpwJAA6cCQAOnAkCnpgJAA6cCQAOnAkCtpgJAs6YCQLmmAkDA29zb3QA+AD8AQABBAEIAQwBEAEUARwBIAEkAAAAAAP83BgAAADgAAIjAKAAAAFMAAAGEAAAAAABAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAMAAAABAAAAAQAAAAAAAAADAAAAAAAAAAEAAAABAAAAAgAAAAIAAAACAAAAAwAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAMAAAAAAAAAAAAAAAAAAQACAAMABAAFAAYABwAIAAkACgALAAwADQAOAA8AEAARABIAEwAUABUAFgAXABgAGQAaABsAHAAdAB4AHwAgACEAIgAjACQAJQAmACcAKABfAF8AXwBfAC0ALgAvADAAMQAyADMANAA1ADYANwA4ADkAOgA7ADwAPQA+AD8AQABBAEIAQwBEAEUARgBHAEgASQBfAEsATABNAE4ATwBQAFEAUgBTAF8AVQBWAFcAWABZAF8AWwBcAF0AXgAAAA==","data_start":1073464320}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/targets/stub_flasher/2/esp32s3.json b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32s3.json
new file mode 100644
index 00000000..8be985b1
--- /dev/null
+++ b/mixly/tools/python/esptool/targets/stub_flasher/2/esp32s3.json
@@ -0,0 +1 @@
+{"entry":1077391268,"text":"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HTAgEzIhHjAhEzIhHzAiEzIhIDAjEzIhJDDo8zIhJTDp8wMBJhMBJyMBKDMBKUMBKlMBK2MBLHMBLYMBLpMBL6MBMLMBMcMBMtMBM+MBNPMBNSIhBDIhBUIhBlIhB2IhCHIhCYIhCpIhC6IhDLIhDcIhDtIhD+IhEPIhEYAAAPBBAD3wEAEgEtH/AmEDANFJAOYDAmEBAOgDAmETAO4DAmEUALEDAmEAAMFJANEDAmECxeD/Adj+AOYTECAAYiETJkYIEHEgVSgARgQAAdP+AOYTECAAYqABEHEgVbX/he7/AiEBAOYTAiEAALETAiECEiEDECAAADAA8EEAEAEgEtH/AmEDANFJAOYDAmEBAOgDAmETAO4DAmEUAMADAmEAAMFJANcDAmECRdn/YiETEHEglSEABen/AiEBAOYTAiEAALETAiECEiEDECAAADIA8EEAABABIBLR/wJhAwDRSQDCAwJhAQCyAwJhAADBSQDSAwJhAoXU/wGp/gDmExAgAGKgAhBxIJWq/8Xj/wIhAQDCEwIhAACyEwIhAhIhAxAgABAyAPBBABABIBLR/wJhAwDRSQDDAwJhAQCzAwJhAADBSQDTAwJhAkXP/wGS/gDmExAgAGKgAxBxIFWl/4Xe/wIhAQDDEwIhAACzEwIhAhIhAxAgABAzAPBBABABIBLR/wJhAwDRSQDEAwJhAQC0AwJhAADBSQDUAwJhAgXK/wGA/gDmExAgAGKgBBBxINW//0XZ/wIhAQDEEwIhAAC0EwIhAhIhAxAgABA0APBBABABIBLR/wJhAwDRSQDFAwJhAQC1AwJhAADBSQDVAwJhAsXE/wFs/gDmExAgAGKgBRBxIJW7/wXU/wIhAQDFEwIhAAC1EwIhAhIhAxAgABA1APBBABABIBLR/wJhAwDRSQDGAwJhAQC2AwJhAADBSQDWAwJhAoW//wFY/gDmExAgAGKgBhBxIFW3/8XO/wIhAQDGEwIhAAC2EwIhAhIhAxAgABA2APBBABABIBLR/wJhAwDRSQDHAwJhAQC3AwJhAADBSQDXAwJhAkW6/wFE/gDmExAgAGKgBxBxIBWz/4XJ/wIhAQDHEwIhAAC3EwIhAhIhAxAgABA3APBBADZBAIEQ/q0CvQPgCAAd8AA2QQDwQQAAAISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAAAAAAISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QAAAAAAAAAAAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0AAAAAAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QAAAAACErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QISsN0CErDdAhKw3QA==","text_start":1077379072,"data":"lZQ3QJmVN0BFljdAlZQ3QJWWN0CZlTdABJc3QDqXN0CKlzdA6Jc3QOmeN0AXmDdA6Z43QEaYN0CVlDdAmZU3QEWWN0DvmDdA+5k3QIeVN0ArmjdAgZo3QIGcN0CZlTdATK43QN+tN0BTrjdAU643QFOuN0A3rjdAU643QFOuN0A9rjdAQ643QEmuN0DA29zb3QAAAAAAAAD/NwYAAAA4AACIwCgAAABTAAABhAAAAAAAQAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAADAAAAAQAAAAEAAAAAAAAAAwAAAAAAAAABAAAAAQAAAAIAAAACAAAAAgAAAAMAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAADAAAAAAAAAAAAAAAAAAEAAgADAAQABQAGAAcACAAJAAoACwAMAA0ADgAPABAAEQASABMAFAAVABYAYwAYABkAGgAbABwAHQAeAB8AIABjAGMAIwAkACUAJgAnACgAKQAqACsALAAtAGMALwAwADEAMgAzADQANQA2ADcAOAA5ADoAOwA8AD0APgA/AEAAQQBCAEMARABFAEYARwBIAEkASgBLAEwATQBjAE8AUABRAFIAUwBUAFUAVgBXAFgAWQBaAFsAXABdAF4AXwBgAGEAYgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAA==","data_start":1070137376}
\ No newline at end of file
diff --git a/mixly/tools/python/esptool/uf2_writer.py b/mixly/tools/python/esptool/uf2_writer.py
new file mode 100644
index 00000000..81772d42
--- /dev/null
+++ b/mixly/tools/python/esptool/uf2_writer.py
@@ -0,0 +1,94 @@
+# SPDX-FileCopyrightText: 2020-2023 Espressif Systems (Shanghai) CO LTD
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Code was originally licensed under Apache 2.0 before the release of ESP-IDF v5.2
+
+import hashlib
+import os
+import struct
+from typing import List
+
+from esptool.util import div_roundup
+
+
+class UF2Writer(object):
+ # The UF2 format is described here: https://github.com/microsoft/uf2
+ UF2_BLOCK_SIZE = 512
+ # max value of CHUNK_SIZE reduced by optional parts. Currently, MD5_PART only.
+ UF2_DATA_SIZE = 476
+ UF2_MD5_PART_SIZE = 24
+ UF2_FIRST_MAGIC = 0x0A324655
+ UF2_SECOND_MAGIC = 0x9E5D5157
+ UF2_FINAL_MAGIC = 0x0AB16F30
+ UF2_FLAG_FAMILYID_PRESENT = 0x00002000
+ UF2_FLAG_MD5_PRESENT = 0x00004000
+
+ def __init__(
+ self,
+ chip_id: int,
+ output_file: os.PathLike,
+ chunk_size: int,
+ md5_enabled: bool = True,
+ ) -> None:
+ if not md5_enabled:
+ self.UF2_MD5_PART_SIZE = 0
+ self.UF2_FLAG_MD5_PRESENT = 0x00000000
+ self.md5_enabled = md5_enabled
+ self.chip_id = chip_id
+ self.CHUNK_SIZE = (
+ self.UF2_DATA_SIZE - self.UF2_MD5_PART_SIZE
+ if chunk_size is None
+ else chunk_size
+ )
+ self.f = open(output_file, "wb")
+
+ def __enter__(self) -> "UF2Writer":
+ return self
+
+ def __exit__(self, exc_type: str, exc_val: int, exc_tb: List) -> None:
+ if self.f:
+ self.f.close()
+
+ @staticmethod
+ def _to_uint32(num: int) -> bytes:
+ return struct.pack(" None:
+ assert len_chunk > 0
+ assert len_chunk <= self.CHUNK_SIZE
+ assert block_no < blocks
+ block = struct.pack(
+ " None:
+ blocks = div_roundup(len(image), self.CHUNK_SIZE)
+ chunks = [
+ image[i : i + self.CHUNK_SIZE]
+ for i in range(0, len(image), self.CHUNK_SIZE)
+ ]
+ for i, chunk in enumerate(chunks):
+ len_chunk = len(chunk)
+ self._write_block(addr, chunk, len_chunk, i, blocks)
+ addr += len_chunk
diff --git a/mixly/tools/python/esptool/util.py b/mixly/tools/python/esptool/util.py
new file mode 100644
index 00000000..15276188
--- /dev/null
+++ b/mixly/tools/python/esptool/util.py
@@ -0,0 +1,206 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import re
+import struct
+import sys
+
+
+def byte(bitstr, index):
+ return bitstr[index]
+
+
+def mask_to_shift(mask):
+ """Return the index of the least significant bit in the mask"""
+ shift = 0
+ while mask & 0x1 == 0:
+ shift += 1
+ mask >>= 1
+ return shift
+
+
+def div_roundup(a, b):
+ """Return a/b rounded up to nearest integer,
+ equivalent result to int(math.ceil(float(int(a)) / float(int(b))), only
+ without possible floating point accuracy errors.
+ """
+ return (int(a) + int(b) - 1) // int(b)
+
+
+def flash_size_bytes(size):
+ """Given a flash size of the type passed in args.flash_size
+ (ie 512KB or 1MB) then return the size in bytes.
+ """
+ if size is None:
+ return None
+ if "MB" in size:
+ return int(size[: size.index("MB")]) * 1024 * 1024
+ elif "KB" in size:
+ return int(size[: size.index("KB")]) * 1024
+ else:
+ raise FatalError("Unknown size %s" % size)
+
+
+def hexify(s, uppercase=True):
+ format_str = "%02X" if uppercase else "%02x"
+ return "".join(format_str % c for c in s)
+
+
+def pad_to(data, alignment, pad_character=b"\xFF"):
+ """Pad to the next alignment boundary"""
+ pad_mod = len(data) % alignment
+ if pad_mod != 0:
+ data += pad_character * (alignment - pad_mod)
+ return data
+
+
+def print_overwrite(message, last_line=False):
+ """Print a message, overwriting the currently printed line.
+
+ If last_line is False, don't append a newline at the end
+ (expecting another subsequent call will overwrite this one.)
+
+ After a sequence of calls with last_line=False, call once with last_line=True.
+
+ If output is not a TTY (for example redirected a pipe),
+ no overwriting happens and this function is the same as print().
+ """
+ if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
+ print("\r%s" % message, end="\n" if last_line else "")
+ else:
+ print(message)
+
+
+def expand_chip_name(chip_name):
+ """Change chip name to official form, e.g. `esp32s3beta2` -> `ESP32-S3(beta2)`"""
+ # Put "-" after "esp32"
+ chip_name = re.sub(r"(esp32)(?!$)", r"\1-", chip_name)
+ # Put "()" around "betaN"
+ chip_name = re.sub(r"(beta\d*)", r"(\1)", chip_name)
+ # Uppercase everything before "(betaN)"
+ chip_name = re.sub(r"^[^\(]+", lambda x: x.group(0).upper(), chip_name)
+ return chip_name
+
+
+def strip_chip_name(chip_name):
+ """Strip chip name to normalized form, e.g. `ESP32-S3(beta2)` -> `esp32s3beta2`"""
+ return re.sub(r"[-()]", "", chip_name.lower())
+
+
+def get_file_size(path_to_file):
+ """Returns the file size in bytes"""
+ file_size = 0
+ with open(path_to_file, "rb") as f:
+ f.seek(0, os.SEEK_END)
+ file_size = f.tell()
+ return file_size
+
+
+class PrintOnce:
+ """
+ Class for printing messages just once. Can be useful when running in a loop
+ """
+
+ def __init__(self) -> None:
+ self.already_printed = False
+
+ def __call__(self, text) -> None:
+ if not self.already_printed:
+ print(text)
+ self.already_printed = True
+
+
+class FatalError(RuntimeError):
+ """
+ Wrapper class for runtime errors that aren't caused by internal bugs, but by
+ ESP ROM responses or input content.
+ """
+
+ def __init__(self, message):
+ RuntimeError.__init__(self, message)
+
+ @staticmethod
+ def WithResult(message, result):
+ """
+ Return a fatal error object that appends the hex values of
+ 'result' and its meaning as a string formatted argument.
+ """
+
+ err_defs = {
+ # ROM error codes
+ 0x101: "Out of memory",
+ 0x102: "Invalid argument",
+ 0x103: "Invalid state",
+ 0x104: "Invalid size",
+ 0x105: "Requested resource not found",
+ 0x106: "Operation or feature not supported",
+ 0x107: "Operation timed out",
+ 0x108: "Received response was invalid",
+ 0x109: "CRC or checksum was invalid",
+ 0x10A: "Version was invalid",
+ 0x10B: "MAC address was invalid",
+ 0x6001: "Flash operation failed",
+ 0x6002: "Flash operation timed out",
+ 0x6003: "Flash not initialised properly",
+ 0x6004: "Operation not supported by the host SPI bus",
+ 0x6005: "Operation not supported by the flash chip",
+ 0x6006: "Can't write, protection enabled",
+ # Flasher stub error codes
+ 0xC000: "Bad data length",
+ 0xC100: "Bad data checksum",
+ 0xC200: "Bad blocksize",
+ 0xC300: "Invalid command",
+ 0xC400: "Failed SPI operation",
+ 0xC500: "Failed SPI unlock",
+ 0xC600: "Not in flash mode",
+ 0xC700: "Inflate error",
+ 0xC800: "Not enough data",
+ 0xC900: "Too much data",
+ 0xFF00: "Command not implemented",
+ }
+
+ err_code = struct.unpack(">H", result[:2])
+ message += " (result was {}: {})".format(
+ hexify(result), err_defs.get(err_code[0], "Unknown result")
+ )
+ return FatalError(message)
+
+
+class NotImplementedInROMError(FatalError):
+ """
+ Wrapper class for the error thrown when a particular ESP bootloader function
+ is not implemented in the ROM bootloader.
+ """
+
+ def __init__(self, bootloader, func):
+ FatalError.__init__(
+ self,
+ "%s ROM does not support function %s."
+ % (bootloader.CHIP_NAME, func.__name__),
+ )
+
+
+class NotSupportedError(FatalError):
+ def __init__(self, esp, function_name):
+ FatalError.__init__(
+ self,
+ f"{function_name} is not supported by {esp.CHIP_NAME}.",
+ )
+
+
+class UnsupportedCommandError(RuntimeError):
+ """
+ Wrapper class for when ROM loader returns an invalid command response.
+
+ Usually this indicates the loader is running in Secure Download Mode.
+ """
+
+ def __init__(self, esp, op):
+ if esp.secure_download_mode:
+ msg = "This command (0x%x) is not supported in Secure Download Mode" % op
+ else:
+ msg = "Invalid (unsupported) command 0x%x" % op
+ RuntimeError.__init__(self, msg)
diff --git a/mixly/tools/python/esptool_main.py b/mixly/tools/python/esptool_main.py
new file mode 100644
index 00000000..30ddb228
--- /dev/null
+++ b/mixly/tools/python/esptool_main.py
@@ -0,0 +1,5 @@
+from esptool import _main
+
+
+if __name__ == "__main__":
+ _main()
\ No newline at end of file
diff --git a/mixly/tools/python/intelhex/__init__.py b/mixly/tools/python/intelhex/__init__.py
new file mode 100644
index 00000000..c6423a6e
--- /dev/null
+++ b/mixly/tools/python/intelhex/__init__.py
@@ -0,0 +1,1372 @@
+# Copyright (c) 2005-2018, Alexander Belchenko
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms,
+# with or without modification, are permitted provided
+# that the following conditions are met:
+#
+# * Redistributions of source code must retain
+# the above copyright notice, this list of conditions
+# and the following disclaimer.
+# * Redistributions in binary form must reproduce
+# the above copyright notice, this list of conditions
+# and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name of the author nor the names
+# of its contributors may be used to endorse
+# or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''Intel HEX format manipulation library.'''
+
+__docformat__ = "javadoc"
+
+from array import array
+from binascii import hexlify, unhexlify
+from bisect import bisect_right
+import os
+import sys
+
+from intelhex.compat import (
+ IntTypes,
+ StrType,
+ StringIO,
+ array_tobytes,
+ asbytes,
+ asstr,
+ dict_items_g,
+ dict_keys,
+ dict_keys_g,
+ range_g,
+ range_l,
+ )
+
+from intelhex.getsizeof import total_size
+
+
+class _DeprecatedParam(object):
+ pass
+
+_DEPRECATED = _DeprecatedParam()
+
+
+class IntelHex(object):
+ ''' Intel HEX file reader. '''
+
+ def __init__(self, source=None):
+ ''' Constructor. If source specified, object will be initialized
+ with the contents of source. Otherwise the object will be empty.
+
+ @param source source for initialization
+ (file name of HEX file, file object, addr dict or
+ other IntelHex object)
+ '''
+ # public members
+ self.padding = 0x0FF
+ # Start Address
+ self.start_addr = None
+
+ # private members
+ self._buf = {}
+ self._offset = 0
+
+ if source is not None:
+ if isinstance(source, StrType) or getattr(source, "read", None):
+ # load hex file
+ self.loadhex(source)
+ elif isinstance(source, dict):
+ self.fromdict(source)
+ elif isinstance(source, IntelHex):
+ self.padding = source.padding
+ if source.start_addr:
+ self.start_addr = source.start_addr.copy()
+ self._buf = source._buf.copy()
+ else:
+ raise ValueError("source: bad initializer type")
+
+ def _decode_record(self, s, line=0):
+ '''Decode one record of HEX file.
+
+ @param s line with HEX record.
+ @param line line number (for error messages).
+
+ @raise EndOfFile if EOF record encountered.
+ '''
+ s = s.rstrip('\r\n')
+ if not s:
+ return # empty line
+
+ if s[0] == ':':
+ try:
+ bin = array('B', unhexlify(asbytes(s[1:])))
+ except (TypeError, ValueError):
+ # this might be raised by unhexlify when odd hexascii digits
+ raise HexRecordError(line=line)
+ length = len(bin)
+ if length < 5:
+ raise HexRecordError(line=line)
+ else:
+ raise HexRecordError(line=line)
+
+ record_length = bin[0]
+ if length != (5 + record_length):
+ raise RecordLengthError(line=line)
+
+ addr = bin[1]*256 + bin[2]
+
+ record_type = bin[3]
+ if not (0 <= record_type <= 5):
+ raise RecordTypeError(line=line)
+
+ crc = sum(bin)
+ crc &= 0x0FF
+ if crc != 0:
+ raise RecordChecksumError(line=line)
+
+ if record_type == 0:
+ # data record
+ addr += self._offset
+ for i in range_g(4, 4+record_length):
+ if not self._buf.get(addr, None) is None:
+ raise AddressOverlapError(address=addr, line=line)
+ self._buf[addr] = bin[i]
+ addr += 1 # FIXME: addr should be wrapped
+ # BUT after 02 record (at 64K boundary)
+ # and after 04 record (at 4G boundary)
+
+ elif record_type == 1:
+ # end of file record
+ if record_length != 0:
+ raise EOFRecordError(line=line)
+ raise _EndOfFile
+
+ elif record_type == 2:
+ # Extended 8086 Segment Record
+ if record_length != 2 or addr != 0:
+ raise ExtendedSegmentAddressRecordError(line=line)
+ self._offset = (bin[4]*256 + bin[5]) * 16
+
+ elif record_type == 4:
+ # Extended Linear Address Record
+ if record_length != 2 or addr != 0:
+ raise ExtendedLinearAddressRecordError(line=line)
+ self._offset = (bin[4]*256 + bin[5]) * 65536
+
+ elif record_type == 3:
+ # Start Segment Address Record
+ if record_length != 4 or addr != 0:
+ raise StartSegmentAddressRecordError(line=line)
+ if self.start_addr:
+ raise DuplicateStartAddressRecordError(line=line)
+ self.start_addr = {'CS': bin[4]*256 + bin[5],
+ 'IP': bin[6]*256 + bin[7],
+ }
+
+ elif record_type == 5:
+ # Start Linear Address Record
+ if record_length != 4 or addr != 0:
+ raise StartLinearAddressRecordError(line=line)
+ if self.start_addr:
+ raise DuplicateStartAddressRecordError(line=line)
+ self.start_addr = {'EIP': (bin[4]*16777216 +
+ bin[5]*65536 +
+ bin[6]*256 +
+ bin[7]),
+ }
+
+ def loadhex(self, fobj):
+ """Load hex file into internal buffer. This is not necessary
+ if object was initialized with source set. This will overwrite
+ addresses if object was already initialized.
+
+ @param fobj file name or file-like object
+ """
+ if getattr(fobj, "read", None) is None:
+ fobj = open(fobj, "r")
+ fclose = fobj.close
+ else:
+ fclose = None
+
+ self._offset = 0
+ line = 0
+
+ try:
+ decode = self._decode_record
+ try:
+ for s in fobj:
+ line += 1
+ decode(s, line)
+ except _EndOfFile:
+ pass
+ finally:
+ if fclose:
+ fclose()
+
+ def loadbin(self, fobj, offset=0):
+ """Load bin file into internal buffer. Not needed if source set in
+ constructor. This will overwrite addresses without warning
+ if object was already initialized.
+
+ @param fobj file name or file-like object
+ @param offset starting address offset
+ """
+ fread = getattr(fobj, "read", None)
+ if fread is None:
+ f = open(fobj, "rb")
+ fread = f.read
+ fclose = f.close
+ else:
+ fclose = None
+
+ try:
+ self.frombytes(array('B', asbytes(fread())), offset=offset)
+ finally:
+ if fclose:
+ fclose()
+
+ def loadfile(self, fobj, format):
+ """Load data file into internal buffer. Preferred wrapper over
+ loadbin or loadhex.
+
+ @param fobj file name or file-like object
+ @param format file format ("hex" or "bin")
+ """
+ if format == "hex":
+ self.loadhex(fobj)
+ elif format == "bin":
+ self.loadbin(fobj)
+ else:
+ raise ValueError('format should be either "hex" or "bin";'
+ ' got %r instead' % format)
+
+ # alias (to be consistent with method tofile)
+ fromfile = loadfile
+
+ def fromdict(self, dikt):
+ """Load data from dictionary. Dictionary should contain int keys
+ representing addresses. Values should be the data to be stored in
+ those addresses in unsigned char form (i.e. not strings).
+ The dictionary may contain the key, ``start_addr``
+ to indicate the starting address of the data as described in README.
+
+ The contents of the dict will be merged with this object and will
+ overwrite any conflicts. This function is not necessary if the
+ object was initialized with source specified.
+ """
+ s = dikt.copy()
+ start_addr = s.get('start_addr')
+ if start_addr is not None:
+ del s['start_addr']
+ for k in dict_keys_g(s):
+ if type(k) not in IntTypes or k < 0:
+ raise ValueError('Source dictionary should have only int keys')
+ self._buf.update(s)
+ if start_addr is not None:
+ self.start_addr = start_addr
+
+ def frombytes(self, bytes, offset=0):
+ """Load data from array or list of bytes.
+ Similar to loadbin() method but works directly with iterable bytes.
+ """
+ for b in bytes:
+ self._buf[offset] = b
+ offset += 1
+
+ def _get_start_end(self, start=None, end=None, size=None):
+ """Return default values for start and end if they are None.
+ If this IntelHex object is empty then it's error to
+ invoke this method with both start and end as None.
+ """
+ if (start,end) == (None,None) and self._buf == {}:
+ raise EmptyIntelHexError
+ if size is not None:
+ if None not in (start, end):
+ raise ValueError("tobinarray: you can't use start,end and size"
+ " arguments in the same time")
+ if (start, end) == (None, None):
+ start = self.minaddr()
+ if start is not None:
+ end = start + size - 1
+ else:
+ start = end - size + 1
+ if start < 0:
+ raise ValueError("tobinarray: invalid size (%d) "
+ "for given end address (%d)" % (size,end))
+ else:
+ if start is None:
+ start = self.minaddr()
+ if end is None:
+ end = self.maxaddr()
+ if start > end:
+ start, end = end, start
+ return start, end
+
+ def tobinarray(self, start=None, end=None, pad=_DEPRECATED, size=None):
+ ''' Convert this object to binary form as array. If start and end
+ unspecified, they will be inferred from the data.
+ @param start start address of output bytes.
+ @param end end address of output bytes (inclusive).
+ @param pad [DEPRECATED PARAMETER, please use self.padding instead]
+ fill empty spaces with this value
+ (if pad is None then this method uses self.padding).
+ @param size size of the block, used with start or end parameter.
+ @return array of unsigned char data.
+ '''
+ if not isinstance(pad, _DeprecatedParam):
+ print ("IntelHex.tobinarray: 'pad' parameter is deprecated.")
+ if pad is not None:
+ print ("Please, use IntelHex.padding attribute instead.")
+ else:
+ print ("Please, don't pass it explicitly.")
+ print ("Use syntax like this: ih.tobinarray(start=xxx, end=yyy, size=zzz)")
+ else:
+ pad = None
+ return self._tobinarray_really(start, end, pad, size)
+
+ def _tobinarray_really(self, start, end, pad, size):
+ """Return binary array."""
+ if pad is None:
+ pad = self.padding
+ bin = array('B')
+ if self._buf == {} and None in (start, end):
+ return bin
+ if size is not None and size <= 0:
+ raise ValueError("tobinarray: wrong value for size")
+ start, end = self._get_start_end(start, end, size)
+ for i in range_g(start, end+1):
+ bin.append(self._buf.get(i, pad))
+ return bin
+
+ def tobinstr(self, start=None, end=None, pad=_DEPRECATED, size=None):
+ ''' Convert to binary form and return as binary string.
+ @param start start address of output bytes.
+ @param end end address of output bytes (inclusive).
+ @param pad [DEPRECATED PARAMETER, please use self.padding instead]
+ fill empty spaces with this value
+ (if pad is None then this method uses self.padding).
+ @param size size of the block, used with start or end parameter.
+ @return bytes string of binary data.
+ '''
+ if not isinstance(pad, _DeprecatedParam):
+ print ("IntelHex.tobinstr: 'pad' parameter is deprecated.")
+ if pad is not None:
+ print ("Please, use IntelHex.padding attribute instead.")
+ else:
+ print ("Please, don't pass it explicitly.")
+ print ("Use syntax like this: ih.tobinstr(start=xxx, end=yyy, size=zzz)")
+ else:
+ pad = None
+ return self._tobinstr_really(start, end, pad, size)
+
+ def _tobinstr_really(self, start, end, pad, size):
+ return array_tobytes(self._tobinarray_really(start, end, pad, size))
+
+ def tobinfile(self, fobj, start=None, end=None, pad=_DEPRECATED, size=None):
+ '''Convert to binary and write to file.
+
+ @param fobj file name or file object for writing output bytes.
+ @param start start address of output bytes.
+ @param end end address of output bytes (inclusive).
+ @param pad [DEPRECATED PARAMETER, please use self.padding instead]
+ fill empty spaces with this value
+ (if pad is None then this method uses self.padding).
+ @param size size of the block, used with start or end parameter.
+ '''
+ if not isinstance(pad, _DeprecatedParam):
+ print ("IntelHex.tobinfile: 'pad' parameter is deprecated.")
+ if pad is not None:
+ print ("Please, use IntelHex.padding attribute instead.")
+ else:
+ print ("Please, don't pass it explicitly.")
+ print ("Use syntax like this: ih.tobinfile(start=xxx, end=yyy, size=zzz)")
+ else:
+ pad = None
+ if getattr(fobj, "write", None) is None:
+ fobj = open(fobj, "wb")
+ close_fd = True
+ else:
+ close_fd = False
+
+ fobj.write(self._tobinstr_really(start, end, pad, size))
+
+ if close_fd:
+ fobj.close()
+
+ def todict(self):
+ '''Convert to python dictionary.
+
+ @return dict suitable for initializing another IntelHex object.
+ '''
+ r = {}
+ r.update(self._buf)
+ if self.start_addr:
+ r['start_addr'] = self.start_addr
+ return r
+
+ def addresses(self):
+ '''Returns all used addresses in sorted order.
+ @return list of occupied data addresses in sorted order.
+ '''
+ aa = dict_keys(self._buf)
+ aa.sort()
+ return aa
+
+ def minaddr(self):
+ '''Get minimal address of HEX content.
+ @return minimal address or None if no data
+ '''
+ aa = dict_keys(self._buf)
+ if aa == []:
+ return None
+ else:
+ return min(aa)
+
+ def maxaddr(self):
+ '''Get maximal address of HEX content.
+ @return maximal address or None if no data
+ '''
+ aa = dict_keys(self._buf)
+ if aa == []:
+ return None
+ else:
+ return max(aa)
+
+ def __getitem__(self, addr):
+ ''' Get requested byte from address.
+ @param addr address of byte.
+ @return byte if address exists in HEX file, or self.padding
+ if no data found.
+ '''
+ t = type(addr)
+ if t in IntTypes:
+ if addr < 0:
+ raise TypeError('Address should be >= 0.')
+ return self._buf.get(addr, self.padding)
+ elif t == slice:
+ addresses = dict_keys(self._buf)
+ ih = IntelHex()
+ if addresses:
+ addresses.sort()
+ start = addr.start or addresses[0]
+ stop = addr.stop or (addresses[-1]+1)
+ step = addr.step or 1
+ for i in range_g(start, stop, step):
+ x = self._buf.get(i)
+ if x is not None:
+ ih[i] = x
+ return ih
+ else:
+ raise TypeError('Address has unsupported type: %s' % t)
+
+ def __setitem__(self, addr, byte):
+ """Set byte at address."""
+ t = type(addr)
+ if t in IntTypes:
+ if addr < 0:
+ raise TypeError('Address should be >= 0.')
+ self._buf[addr] = byte
+ elif t == slice:
+ if not isinstance(byte, (list, tuple)):
+ raise ValueError('Slice operation expects sequence of bytes')
+ start = addr.start
+ stop = addr.stop
+ step = addr.step or 1
+ if None not in (start, stop):
+ ra = range_l(start, stop, step)
+ if len(ra) != len(byte):
+ raise ValueError('Length of bytes sequence does not match '
+ 'address range')
+ elif (start, stop) == (None, None):
+ raise TypeError('Unsupported address range')
+ elif start is None:
+ start = stop - len(byte)
+ elif stop is None:
+ stop = start + len(byte)
+ if start < 0:
+ raise TypeError('start address cannot be negative')
+ if stop < 0:
+ raise TypeError('stop address cannot be negative')
+ j = 0
+ for i in range_g(start, stop, step):
+ self._buf[i] = byte[j]
+ j += 1
+ else:
+ raise TypeError('Address has unsupported type: %s' % t)
+
+ def __delitem__(self, addr):
+ """Delete byte at address."""
+ t = type(addr)
+ if t in IntTypes:
+ if addr < 0:
+ raise TypeError('Address should be >= 0.')
+ del self._buf[addr]
+ elif t == slice:
+ addresses = dict_keys(self._buf)
+ if addresses:
+ addresses.sort()
+ start = addr.start or addresses[0]
+ stop = addr.stop or (addresses[-1]+1)
+ step = addr.step or 1
+ for i in range_g(start, stop, step):
+ x = self._buf.get(i)
+ if x is not None:
+ del self._buf[i]
+ else:
+ raise TypeError('Address has unsupported type: %s' % t)
+
+ def __len__(self):
+ """Return count of bytes with real values."""
+ return len(dict_keys(self._buf))
+
+ def _get_eol_textfile(eolstyle, platform):
+ if eolstyle == 'native':
+ return '\n'
+ elif eolstyle == 'CRLF':
+ if platform != 'win32':
+ return '\r\n'
+ else:
+ return '\n'
+ else:
+ raise ValueError("wrong eolstyle %s" % repr(eolstyle))
+ _get_eol_textfile = staticmethod(_get_eol_textfile)
+
+ def write_hex_file(self, f, write_start_addr=True, eolstyle='native', byte_count=16):
+ """Write data to file f in HEX format.
+
+ @param f filename or file-like object for writing
+ @param write_start_addr enable or disable writing start address
+ record to file (enabled by default).
+ If there is no start address in obj, nothing
+ will be written regardless of this setting.
+ @param eolstyle can be used to force CRLF line-endings
+ for output file on different platforms.
+ Supported eol styles: 'native', 'CRLF'.
+ @param byte_count number of bytes in the data field
+ """
+ if byte_count > 255 or byte_count < 1:
+ raise ValueError("wrong byte_count value: %s" % byte_count)
+ fwrite = getattr(f, "write", None)
+ if fwrite:
+ fobj = f
+ fclose = None
+ else:
+ fobj = open(f, 'w')
+ fwrite = fobj.write
+ fclose = fobj.close
+
+ eol = IntelHex._get_eol_textfile(eolstyle, sys.platform)
+
+ # Translation table for uppercasing hex ascii string.
+ # timeit shows that using hexstr.translate(table)
+ # is faster than hexstr.upper():
+ # 0.452ms vs. 0.652ms (translate vs. upper)
+ if sys.version_info[0] >= 3:
+ # Python 3
+ table = bytes(range_l(256)).upper()
+ else:
+ # Python 2
+ table = ''.join(chr(i).upper() for i in range_g(256))
+
+ # start address record if any
+ if self.start_addr and write_start_addr:
+ keys = dict_keys(self.start_addr)
+ keys.sort()
+ bin = array('B', asbytes('\0'*9))
+ if keys == ['CS','IP']:
+ # Start Segment Address Record
+ bin[0] = 4 # reclen
+ bin[1] = 0 # offset msb
+ bin[2] = 0 # offset lsb
+ bin[3] = 3 # rectyp
+ cs = self.start_addr['CS']
+ bin[4] = (cs >> 8) & 0x0FF
+ bin[5] = cs & 0x0FF
+ ip = self.start_addr['IP']
+ bin[6] = (ip >> 8) & 0x0FF
+ bin[7] = ip & 0x0FF
+ bin[8] = (-sum(bin)) & 0x0FF # chksum
+ fwrite(':' +
+ asstr(hexlify(array_tobytes(bin)).translate(table)) +
+ eol)
+ elif keys == ['EIP']:
+ # Start Linear Address Record
+ bin[0] = 4 # reclen
+ bin[1] = 0 # offset msb
+ bin[2] = 0 # offset lsb
+ bin[3] = 5 # rectyp
+ eip = self.start_addr['EIP']
+ bin[4] = (eip >> 24) & 0x0FF
+ bin[5] = (eip >> 16) & 0x0FF
+ bin[6] = (eip >> 8) & 0x0FF
+ bin[7] = eip & 0x0FF
+ bin[8] = (-sum(bin)) & 0x0FF # chksum
+ fwrite(':' +
+ asstr(hexlify(array_tobytes(bin)).translate(table)) +
+ eol)
+ else:
+ if fclose:
+ fclose()
+ raise InvalidStartAddressValueError(start_addr=self.start_addr)
+
+ # data
+ addresses = dict_keys(self._buf)
+ addresses.sort()
+ addr_len = len(addresses)
+ if addr_len:
+ minaddr = addresses[0]
+ maxaddr = addresses[-1]
+
+ if maxaddr > 65535:
+ need_offset_record = True
+ else:
+ need_offset_record = False
+ high_ofs = 0
+
+ cur_addr = minaddr
+ cur_ix = 0
+
+ while cur_addr <= maxaddr:
+ if need_offset_record:
+ bin = array('B', asbytes('\0'*7))
+ bin[0] = 2 # reclen
+ bin[1] = 0 # offset msb
+ bin[2] = 0 # offset lsb
+ bin[3] = 4 # rectyp
+ high_ofs = int(cur_addr>>16)
+ b = divmod(high_ofs, 256)
+ bin[4] = b[0] # msb of high_ofs
+ bin[5] = b[1] # lsb of high_ofs
+ bin[6] = (-sum(bin)) & 0x0FF # chksum
+ fwrite(':' +
+ asstr(hexlify(array_tobytes(bin)).translate(table)) +
+ eol)
+
+ while True:
+ # produce one record
+ low_addr = cur_addr & 0x0FFFF
+ # chain_len off by 1
+ chain_len = min(byte_count-1, 65535-low_addr, maxaddr-cur_addr)
+
+ # search continuous chain
+ stop_addr = cur_addr + chain_len
+ if chain_len:
+ ix = bisect_right(addresses, stop_addr,
+ cur_ix,
+ min(cur_ix+chain_len+1, addr_len))
+ chain_len = ix - cur_ix # real chain_len
+ # there could be small holes in the chain
+ # but we will catch them by try-except later
+ # so for big continuous files we will work
+ # at maximum possible speed
+ else:
+ chain_len = 1 # real chain_len
+
+ bin = array('B', asbytes('\0'*(5+chain_len)))
+ b = divmod(low_addr, 256)
+ bin[1] = b[0] # msb of low_addr
+ bin[2] = b[1] # lsb of low_addr
+ bin[3] = 0 # rectype
+ try: # if there is small holes we'll catch them
+ for i in range_g(chain_len):
+ bin[4+i] = self._buf[cur_addr+i]
+ except KeyError:
+ # we catch a hole so we should shrink the chain
+ chain_len = i
+ bin = bin[:5+i]
+ bin[0] = chain_len
+ bin[4+chain_len] = (-sum(bin)) & 0x0FF # chksum
+ fwrite(':' +
+ asstr(hexlify(array_tobytes(bin)).translate(table)) +
+ eol)
+
+ # adjust cur_addr/cur_ix
+ cur_ix += chain_len
+ if cur_ix < addr_len:
+ cur_addr = addresses[cur_ix]
+ else:
+ cur_addr = maxaddr + 1
+ break
+ high_addr = int(cur_addr>>16)
+ if high_addr > high_ofs:
+ break
+
+ # end-of-file record
+ fwrite(":00000001FF"+eol)
+ if fclose:
+ fclose()
+
+ def tofile(self, fobj, format, byte_count=16):
+ """Write data to hex or bin file. Preferred method over tobin or tohex.
+
+ @param fobj file name or file-like object
+ @param format file format ("hex" or "bin")
+ @param byte_count bytes per line
+ """
+ if format == 'hex':
+ self.write_hex_file(fobj, byte_count=byte_count)
+ elif format == 'bin':
+ self.tobinfile(fobj)
+ else:
+ raise ValueError('format should be either "hex" or "bin";'
+ ' got %r instead' % format)
+
+ def gets(self, addr, length):
+ """Get string of bytes from given address. If any entries are blank
+ from addr through addr+length, a NotEnoughDataError exception will
+ be raised. Padding is not used.
+ """
+ a = array('B', asbytes('\0'*length))
+ try:
+ for i in range_g(length):
+ a[i] = self._buf[addr+i]
+ except KeyError:
+ raise NotEnoughDataError(address=addr, length=length)
+ return array_tobytes(a)
+
+ def puts(self, addr, s):
+ """Put string of bytes at given address. Will overwrite any previous
+ entries.
+ """
+ a = array('B', asbytes(s))
+ for i in range_g(len(a)):
+ self._buf[addr+i] = a[i]
+
+ def getsz(self, addr):
+ """Get zero-terminated bytes string from given address. Will raise
+ NotEnoughDataError exception if a hole is encountered before a 0.
+ """
+ i = 0
+ try:
+ while True:
+ if self._buf[addr+i] == 0:
+ break
+ i += 1
+ except KeyError:
+ raise NotEnoughDataError(msg=('Bad access at 0x%X: '
+ 'not enough data to read zero-terminated string') % addr)
+ return self.gets(addr, i)
+
+ def putsz(self, addr, s):
+ """Put bytes string in object at addr and append terminating zero at end."""
+ self.puts(addr, s)
+ self._buf[addr+len(s)] = 0
+
+ def find(self, sub, start=None, end=None):
+ """Return the lowest index in self[start:end] where subsection sub is found.
+ Optional arguments start and end are interpreted as in slice notation.
+
+ @param sub bytes-like subsection to find
+ @param start start of section to search within (optional)
+ @param end end of section to search within (optional)
+ """
+ sub = bytes(sub)
+ for start, end in self[slice(start,end)].segments():
+ b = self.gets(start, end-start)
+ i = b.find(sub)
+ if i != -1:
+ return start+i
+ return -1
+
+ def dump(self, tofile=None, width=16, withpadding=False):
+ """Dump object content to specified file object or to stdout if None.
+ Format is a hexdump with some header information at the beginning,
+ addresses on the left, and data on right.
+
+ @param tofile file-like object to dump to
+ @param width number of bytes per line (i.e. columns)
+ @param withpadding print padding character instead of '--'
+ @raise ValueError if width is not a positive integer
+ """
+
+ if not isinstance(width,int) or width < 1:
+ raise ValueError('width must be a positive integer.')
+ # The integer can be of float type - does not work with bit operations
+ width = int(width)
+ if tofile is None:
+ tofile = sys.stdout
+
+ # start addr possibly
+ if self.start_addr is not None:
+ cs = self.start_addr.get('CS')
+ ip = self.start_addr.get('IP')
+ eip = self.start_addr.get('EIP')
+ if eip is not None and cs is None and ip is None:
+ tofile.write('EIP = 0x%08X\n' % eip)
+ elif eip is None and cs is not None and ip is not None:
+ tofile.write('CS = 0x%04X, IP = 0x%04X\n' % (cs, ip))
+ else:
+ tofile.write('start_addr = %r\n' % start_addr)
+ # actual data
+ addresses = dict_keys(self._buf)
+ if addresses:
+ addresses.sort()
+ minaddr = addresses[0]
+ maxaddr = addresses[-1]
+ startaddr = (minaddr // width) * width
+ endaddr = ((maxaddr // width) + 1) * width
+ maxdigits = max(len(hex(endaddr)) - 2, 4) # Less 2 to exclude '0x'
+ templa = '%%0%dX' % maxdigits
+ rangewidth = range_l(width)
+ if withpadding:
+ pad = self.padding
+ else:
+ pad = None
+ for i in range_g(startaddr, endaddr, width):
+ tofile.write(templa % i)
+ tofile.write(' ')
+ s = []
+ for j in rangewidth:
+ x = self._buf.get(i+j, pad)
+ if x is not None:
+ tofile.write(' %02X' % x)
+ if 32 <= x < 127: # GNU less does not like 0x7F (128 decimal) so we'd better show it as dot
+ s.append(chr(x))
+ else:
+ s.append('.')
+ else:
+ tofile.write(' --')
+ s.append(' ')
+ tofile.write(' |' + ''.join(s) + '|\n')
+
+ def merge(self, other, overlap='error'):
+ """Merge content of other IntelHex object into current object (self).
+ @param other other IntelHex object.
+ @param overlap action on overlap of data or starting addr:
+ - error: raising OverlapError;
+ - ignore: ignore other data and keep current data
+ in overlapping region;
+ - replace: replace data with other data
+ in overlapping region.
+
+ @raise TypeError if other is not instance of IntelHex
+ @raise ValueError if other is the same object as self
+ (it can't merge itself)
+ @raise ValueError if overlap argument has incorrect value
+ @raise AddressOverlapError on overlapped data
+ """
+ # check args
+ if not isinstance(other, IntelHex):
+ raise TypeError('other should be IntelHex object')
+ if other is self:
+ raise ValueError("Can't merge itself")
+ if overlap not in ('error', 'ignore', 'replace'):
+ raise ValueError("overlap argument should be either "
+ "'error', 'ignore' or 'replace'")
+ # merge data
+ this_buf = self._buf
+ other_buf = other._buf
+ for i in other_buf:
+ if i in this_buf:
+ if overlap == 'error':
+ raise AddressOverlapError(
+ 'Data overlapped at address 0x%X' % i)
+ elif overlap == 'ignore':
+ continue
+ this_buf[i] = other_buf[i]
+ # merge start_addr
+ if self.start_addr != other.start_addr:
+ if self.start_addr is None: # set start addr from other
+ self.start_addr = other.start_addr
+ elif other.start_addr is None: # keep existing start addr
+ pass
+ else: # conflict
+ if overlap == 'error':
+ raise AddressOverlapError(
+ 'Starting addresses are different')
+ elif overlap == 'replace':
+ self.start_addr = other.start_addr
+
+ def segments(self, min_gap=1):
+ """Return a list of ordered tuple objects, representing contiguous occupied data addresses.
+ Each tuple has a length of two and follows the semantics of the range and xrange objects.
+ The second entry of the tuple is always an integer greater than the first entry.
+ @param min_gap the minimum gap size between data in order to separate the segments
+ """
+ addresses = self.addresses()
+ if not addresses:
+ return []
+ elif len(addresses) == 1:
+ return([(addresses[0], addresses[0]+1)])
+ adjacent_differences = [(b - a) for (a, b) in zip(addresses[:-1], addresses[1:])]
+ breaks = [i for (i, x) in enumerate(adjacent_differences) if x > min_gap]
+ endings = [addresses[b] for b in breaks]
+ endings.append(addresses[-1])
+ beginnings = [addresses[b+1] for b in breaks]
+ beginnings.insert(0, addresses[0])
+ return [(a, b+1) for (a, b) in zip(beginnings, endings)]
+
+ def get_memory_size(self):
+ """Returns the approximate memory footprint for data."""
+ n = sys.getsizeof(self)
+ n += sys.getsizeof(self.padding)
+ n += total_size(self.start_addr)
+ n += total_size(self._buf)
+ n += sys.getsizeof(self._offset)
+ return n
+
+#/IntelHex
+
+
+class IntelHex16bit(IntelHex):
+ """Access to data as 16-bit words. Intended to use with Microchip HEX files."""
+
+ def __init__(self, source=None):
+ """Construct class from HEX file
+ or from instance of ordinary IntelHex class. If IntelHex object
+ is passed as source, the original IntelHex object should not be used
+ again because this class will alter it. This class leaves padding
+ alone unless it was precisely 0xFF. In that instance it is sign
+ extended to 0xFFFF.
+
+ @param source file name of HEX file or file object
+ or instance of ordinary IntelHex class.
+ Will also accept dictionary from todict method.
+ """
+ if isinstance(source, IntelHex):
+ # from ihex8
+ self.padding = source.padding
+ self.start_addr = source.start_addr
+ # private members
+ self._buf = source._buf
+ self._offset = source._offset
+ elif isinstance(source, dict):
+ raise IntelHexError("IntelHex16bit does not support initialization from dictionary yet.\n"
+ "Patches are welcome.")
+ else:
+ IntelHex.__init__(self, source)
+
+ if self.padding == 0x0FF:
+ self.padding = 0x0FFFF
+
+ def __getitem__(self, addr16):
+ """Get 16-bit word from address.
+ Raise error if only one byte from the pair is set.
+ We assume a Little Endian interpretation of the hex file.
+
+ @param addr16 address of word (addr8 = 2 * addr16).
+ @return word if bytes exists in HEX file, or self.padding
+ if no data found.
+ """
+ addr1 = addr16 * 2
+ addr2 = addr1 + 1
+ byte1 = self._buf.get(addr1, None)
+ byte2 = self._buf.get(addr2, None)
+
+ if byte1 != None and byte2 != None:
+ return byte1 | (byte2 << 8) # low endian
+
+ if byte1 == None and byte2 == None:
+ return self.padding
+
+ raise BadAccess16bit(address=addr16)
+
+ def __setitem__(self, addr16, word):
+ """Sets the address at addr16 to word assuming Little Endian mode.
+ """
+ addr_byte = addr16 * 2
+ b = divmod(word, 256)
+ self._buf[addr_byte] = b[1]
+ self._buf[addr_byte+1] = b[0]
+
+ def minaddr(self):
+ '''Get minimal address of HEX content in 16-bit mode.
+
+ @return minimal address used in this object
+ '''
+ aa = dict_keys(self._buf)
+ if aa == []:
+ return 0
+ else:
+ return min(aa)>>1
+
+ def maxaddr(self):
+ '''Get maximal address of HEX content in 16-bit mode.
+
+ @return maximal address used in this object
+ '''
+ aa = dict_keys(self._buf)
+ if aa == []:
+ return 0
+ else:
+ return max(aa)>>1
+
+ def tobinarray(self, start=None, end=None, size=None):
+ '''Convert this object to binary form as array (of 2-bytes word data).
+ If start and end unspecified, they will be inferred from the data.
+ @param start start address of output data.
+ @param end end address of output data (inclusive).
+ @param size size of the block (number of words),
+ used with start or end parameter.
+ @return array of unsigned short (uint16_t) data.
+ '''
+ bin = array('H')
+
+ if self._buf == {} and None in (start, end):
+ return bin
+
+ if size is not None and size <= 0:
+ raise ValueError("tobinarray: wrong value for size")
+
+ start, end = self._get_start_end(start, end, size)
+
+ for addr in range_g(start, end+1):
+ bin.append(self[addr])
+
+ return bin
+
+
+#/class IntelHex16bit
+
+
+def hex2bin(fin, fout, start=None, end=None, size=None, pad=None):
+ """Hex-to-Bin convertor engine.
+ @return 0 if all OK
+
+ @param fin input hex file (filename or file-like object)
+ @param fout output bin file (filename or file-like object)
+ @param start start of address range (optional)
+ @param end end of address range (inclusive; optional)
+ @param size size of resulting file (in bytes) (optional)
+ @param pad padding byte (optional)
+ """
+ try:
+ h = IntelHex(fin)
+ except HexReaderError:
+ e = sys.exc_info()[1] # current exception
+ txt = "ERROR: bad HEX file: %s" % str(e)
+ print(txt)
+ return 1
+
+ # start, end, size
+ if size != None and size != 0:
+ if end == None:
+ if start == None:
+ start = h.minaddr()
+ end = start + size - 1
+ else:
+ if (end+1) >= size:
+ start = end + 1 - size
+ else:
+ start = 0
+
+ try:
+ if pad is not None:
+ # using .padding attribute rather than pad argument to function call
+ h.padding = pad
+ h.tobinfile(fout, start, end)
+ except IOError:
+ e = sys.exc_info()[1] # current exception
+ txt = "ERROR: Could not write to file: %s: %s" % (fout, str(e))
+ print(txt)
+ return 1
+
+ return 0
+#/def hex2bin
+
+
+def bin2hex(fin, fout, offset=0):
+ """Simple bin-to-hex convertor.
+ @return 0 if all OK
+
+ @param fin input bin file (filename or file-like object)
+ @param fout output hex file (filename or file-like object)
+ @param offset starting address offset for loading bin
+ """
+ h = IntelHex()
+ try:
+ h.loadbin(fin, offset)
+ except IOError:
+ e = sys.exc_info()[1] # current exception
+ txt = 'ERROR: unable to load bin file:', str(e)
+ print(txt)
+ return 1
+
+ try:
+ h.tofile(fout, format='hex')
+ except IOError:
+ e = sys.exc_info()[1] # current exception
+ txt = "ERROR: Could not write to file: %s: %s" % (fout, str(e))
+ print(txt)
+ return 1
+
+ return 0
+#/def bin2hex
+
+
+def diff_dumps(ih1, ih2, tofile=None, name1="a", name2="b", n_context=3):
+ """Diff 2 IntelHex objects and produce unified diff output for their
+ hex dumps.
+
+ @param ih1 first IntelHex object to compare
+ @param ih2 second IntelHex object to compare
+ @param tofile file-like object to write output
+ @param name1 name of the first hex file to show in the diff header
+ @param name2 name of the first hex file to show in the diff header
+ @param n_context number of context lines in the unidiff output
+ """
+ def prepare_lines(ih):
+ sio = StringIO()
+ ih.dump(sio)
+ dump = sio.getvalue()
+ lines = dump.splitlines()
+ return lines
+ a = prepare_lines(ih1)
+ b = prepare_lines(ih2)
+ import difflib
+ result = list(difflib.unified_diff(a, b, fromfile=name1, tofile=name2, n=n_context, lineterm=''))
+ if tofile is None:
+ tofile = sys.stdout
+ output = '\n'.join(result)+'\n'
+ tofile.write(output)
+
+
+class Record(object):
+ """Helper methods to build valid ihex records."""
+
+ def _from_bytes(bytes):
+ """Takes a list of bytes, computes the checksum, and outputs the entire
+ record as a string. bytes should be the hex record without the colon
+ or final checksum.
+
+ @param bytes list of byte values so far to pack into record.
+ @return String representation of one HEX record
+ """
+ assert len(bytes) >= 4
+ # calculate checksum
+ s = (-sum(bytes)) & 0x0FF
+ bin = array('B', bytes + [s])
+ return ':' + asstr(hexlify(array_tobytes(bin))).upper()
+ _from_bytes = staticmethod(_from_bytes)
+
+ def data(offset, bytes):
+ """Return Data record. This constructs the full record, including
+ the length information, the record type (0x00), the
+ checksum, and the offset.
+
+ @param offset load offset of first byte.
+ @param bytes list of byte values to pack into record.
+
+ @return String representation of one HEX record
+ """
+ assert 0 <= offset < 65536
+ assert 0 < len(bytes) < 256
+ b = [len(bytes), (offset>>8)&0x0FF, offset&0x0FF, 0x00] + bytes
+ return Record._from_bytes(b)
+ data = staticmethod(data)
+
+ def eof():
+ """Return End of File record as a string.
+ @return String representation of Intel Hex EOF record
+ """
+ return ':00000001FF'
+ eof = staticmethod(eof)
+
+ def extended_segment_address(usba):
+ """Return Extended Segment Address Record.
+ @param usba Upper Segment Base Address.
+
+ @return String representation of Intel Hex USBA record.
+ """
+ b = [2, 0, 0, 0x02, (usba>>8)&0x0FF, usba&0x0FF]
+ return Record._from_bytes(b)
+ extended_segment_address = staticmethod(extended_segment_address)
+
+ def start_segment_address(cs, ip):
+ """Return Start Segment Address Record.
+ @param cs 16-bit value for CS register.
+ @param ip 16-bit value for IP register.
+
+ @return String representation of Intel Hex SSA record.
+ """
+ b = [4, 0, 0, 0x03, (cs>>8)&0x0FF, cs&0x0FF,
+ (ip>>8)&0x0FF, ip&0x0FF]
+ return Record._from_bytes(b)
+ start_segment_address = staticmethod(start_segment_address)
+
+ def extended_linear_address(ulba):
+ """Return Extended Linear Address Record.
+ @param ulba Upper Linear Base Address.
+
+ @return String representation of Intel Hex ELA record.
+ """
+ b = [2, 0, 0, 0x04, (ulba>>8)&0x0FF, ulba&0x0FF]
+ return Record._from_bytes(b)
+ extended_linear_address = staticmethod(extended_linear_address)
+
+ def start_linear_address(eip):
+ """Return Start Linear Address Record.
+ @param eip 32-bit linear address for the EIP register.
+
+ @return String representation of Intel Hex SLA record.
+ """
+ b = [4, 0, 0, 0x05, (eip>>24)&0x0FF, (eip>>16)&0x0FF,
+ (eip>>8)&0x0FF, eip&0x0FF]
+ return Record._from_bytes(b)
+ start_linear_address = staticmethod(start_linear_address)
+
+
+class _BadFileNotation(Exception):
+ """Special error class to use with _get_file_and_addr_range."""
+ pass
+
+def _get_file_and_addr_range(s, _support_drive_letter=None):
+ """Special method for hexmerge.py script to split file notation
+ into 3 parts: (filename, start, end)
+
+ @raise _BadFileNotation when string cannot be safely split.
+ """
+ if _support_drive_letter is None:
+ _support_drive_letter = (os.name == 'nt')
+ drive = ''
+ if _support_drive_letter:
+ if s[1:2] == ':' and s[0].upper() in ''.join([chr(i) for i in range_g(ord('A'), ord('Z')+1)]):
+ drive = s[:2]
+ s = s[2:]
+ parts = s.split(':')
+ n = len(parts)
+ if n == 1:
+ fname = parts[0]
+ fstart = None
+ fend = None
+ elif n != 3:
+ raise _BadFileNotation
+ else:
+ fname = parts[0]
+ def ascii_hex_to_int(ascii):
+ if ascii is not None:
+ try:
+ return int(ascii, 16)
+ except ValueError:
+ raise _BadFileNotation
+ return ascii
+ fstart = ascii_hex_to_int(parts[1] or None)
+ fend = ascii_hex_to_int(parts[2] or None)
+ return drive+fname, fstart, fend
+
+
+##
+# IntelHex Errors Hierarchy:
+#
+# IntelHexError - basic error
+# HexReaderError - general hex reader error
+# AddressOverlapError - data for the same address overlap
+# HexRecordError - hex record decoder base error
+# RecordLengthError - record has invalid length
+# RecordTypeError - record has invalid type (RECTYP)
+# RecordChecksumError - record checksum mismatch
+# EOFRecordError - invalid EOF record (type 01)
+# ExtendedAddressRecordError - extended address record base error
+# ExtendedSegmentAddressRecordError - invalid extended segment address record (type 02)
+# ExtendedLinearAddressRecordError - invalid extended linear address record (type 04)
+# StartAddressRecordError - start address record base error
+# StartSegmentAddressRecordError - invalid start segment address record (type 03)
+# StartLinearAddressRecordError - invalid start linear address record (type 05)
+# DuplicateStartAddressRecordError - start address record appears twice
+# InvalidStartAddressValueError - invalid value of start addr record
+# _EndOfFile - it's not real error, used internally by hex reader as signal that EOF record found
+# BadAccess16bit - not enough data to read 16 bit value (deprecated, see NotEnoughDataError)
+# NotEnoughDataError - not enough data to read N contiguous bytes
+# EmptyIntelHexError - requested operation cannot be performed with empty object
+
+class IntelHexError(Exception):
+ '''Base Exception class for IntelHex module'''
+
+ _fmt = 'IntelHex base error' #: format string
+
+ def __init__(self, msg=None, **kw):
+ """Initialize the Exception with the given message.
+ """
+ self.msg = msg
+ for key, value in dict_items_g(kw):
+ setattr(self, key, value)
+
+ def __str__(self):
+ """Return the message in this Exception."""
+ if self.msg:
+ return self.msg
+ try:
+ return self._fmt % self.__dict__
+ except (NameError, ValueError, KeyError):
+ e = sys.exc_info()[1] # current exception
+ return 'Unprintable exception %s: %s' \
+ % (repr(e), str(e))
+
+class _EndOfFile(IntelHexError):
+ """Used for internal needs only."""
+ _fmt = 'EOF record reached -- signal to stop read file'
+
+class HexReaderError(IntelHexError):
+ _fmt = 'Hex reader base error'
+
+class AddressOverlapError(HexReaderError):
+ _fmt = 'Hex file has data overlap at address 0x%(address)X on line %(line)d'
+
+# class NotAHexFileError was removed in trunk.revno.54 because it's not used
+
+
+class HexRecordError(HexReaderError):
+ _fmt = 'Hex file contains invalid record at line %(line)d'
+
+
+class RecordLengthError(HexRecordError):
+ _fmt = 'Record at line %(line)d has invalid length'
+
+class RecordTypeError(HexRecordError):
+ _fmt = 'Record at line %(line)d has invalid record type'
+
+class RecordChecksumError(HexRecordError):
+ _fmt = 'Record at line %(line)d has invalid checksum'
+
+class EOFRecordError(HexRecordError):
+ _fmt = 'File has invalid End-of-File record'
+
+
+class ExtendedAddressRecordError(HexRecordError):
+ _fmt = 'Base class for extended address exceptions'
+
+class ExtendedSegmentAddressRecordError(ExtendedAddressRecordError):
+ _fmt = 'Invalid Extended Segment Address Record at line %(line)d'
+
+class ExtendedLinearAddressRecordError(ExtendedAddressRecordError):
+ _fmt = 'Invalid Extended Linear Address Record at line %(line)d'
+
+
+class StartAddressRecordError(HexRecordError):
+ _fmt = 'Base class for start address exceptions'
+
+class StartSegmentAddressRecordError(StartAddressRecordError):
+ _fmt = 'Invalid Start Segment Address Record at line %(line)d'
+
+class StartLinearAddressRecordError(StartAddressRecordError):
+ _fmt = 'Invalid Start Linear Address Record at line %(line)d'
+
+class DuplicateStartAddressRecordError(StartAddressRecordError):
+ _fmt = 'Start Address Record appears twice at line %(line)d'
+
+class InvalidStartAddressValueError(StartAddressRecordError):
+ _fmt = 'Invalid start address value: %(start_addr)s'
+
+
+class NotEnoughDataError(IntelHexError):
+ _fmt = ('Bad access at 0x%(address)X: '
+ 'not enough data to read %(length)d contiguous bytes')
+
+class BadAccess16bit(NotEnoughDataError):
+ _fmt = 'Bad access at 0x%(address)X: not enough data to read 16 bit value'
+
+class EmptyIntelHexError(IntelHexError):
+ _fmt = "Requested operation cannot be executed with empty object"
diff --git a/mixly/tools/python/intelhex/__main__.py b/mixly/tools/python/intelhex/__main__.py
new file mode 100644
index 00000000..50db2840
--- /dev/null
+++ b/mixly/tools/python/intelhex/__main__.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2016-2018, Alexander Belchenko
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms,
+# with or without modification, are permitted provided
+# that the following conditions are met:
+#
+# * Redistributions of source code must retain
+# the above copyright notice, this list of conditions
+# and the following disclaimer.
+# * Redistributions in binary form must reproduce
+# the above copyright notice, this list of conditions
+# and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name of the author nor the names
+# of its contributors may be used to endorse
+# or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+if __name__ == '__main__':
+ print("Welcome to IntelHex Python library.")
+ print()
+ print("The intelhex package has some executable points:")
+ print(" python -m intelhex.test -- easy way to run unit tests.")
+ print(" python -m intelhex.bench -- run benchmarks.")
diff --git a/mixly/tools/python/intelhex/__version__.py b/mixly/tools/python/intelhex/__version__.py
new file mode 100644
index 00000000..36edd5c2
--- /dev/null
+++ b/mixly/tools/python/intelhex/__version__.py
@@ -0,0 +1,3 @@
+# IntelHex library version information
+version_info = (2, 3, 0)
+version_str = '.'.join([str(i) for i in version_info])
diff --git a/mixly/tools/python/intelhex/bench.py b/mixly/tools/python/intelhex/bench.py
new file mode 100644
index 00000000..df484479
--- /dev/null
+++ b/mixly/tools/python/intelhex/bench.py
@@ -0,0 +1,360 @@
+#!/usr/bin/python
+# (c) Alexander Belchenko, 2007, 2009
+
+# [2013/08] NOTE: This file is keeping for historical reasons.
+# It may or may not work actually with current version of intelhex,
+# and most likely it requires some fixes here and there.
+
+"""Benchmarking.
+
+Run each test 3 times and get median value.
+Using 10K array as base test time.
+
+Each other test compared with base with next formula::
+
+ Tc * Nb
+ q = ---------
+ Tb * Nc
+
+Here:
+
+* Tc - execution time of current test
+* Tb - execution time of base
+* Nb - array size of base (10K)
+* Nc - array size of current test
+
+If resulting value is ``q <= 1.0`` it's the best possible result,
+i.e. time increase proportionally to array size.
+"""
+
+import gc
+import sys
+import time
+
+import intelhex
+from intelhex.compat import StringIO, range_g
+
+def median(values):
+ """Return median value for the list of values.
+ @param values: list of values for processing.
+ @return: median value.
+ """
+ values.sort()
+ n = int(len(values) / 2)
+ return values[n]
+
+def run_test(func, fobj):
+ """Run func with argument fobj and measure execution time.
+ @param func: function for test
+ @param fobj: data for test
+ @return: execution time
+ """
+ gc.disable()
+ try:
+ begin = time.time()
+ func(fobj)
+ end = time.time()
+ finally:
+ gc.enable()
+ return end - begin
+
+def run_readtest_N_times(func, hexstr, n):
+ """Run each test N times.
+ @param func: function for test
+ @param hexstr: string with content of hex file to read
+ @param n: times to repeat.
+ @return: (median time, times list)
+ """
+ assert n > 0
+ times = []
+ for i in range_g(n):
+ sio = StringIO(hexstr)
+ times.append(run_test(func, sio))
+ sio.close()
+ t = median(times)
+ return t, times
+
+def run_writetest_N_times(func, n):
+ """Run each test N times.
+ @param func: function for test
+ @param n: times to repeat.
+ @return: (median time, times list)
+ """
+ assert n > 0
+ times = []
+ for i in range_g(n):
+ sio = StringIO()
+ times.append(run_test(func, sio))
+ sio.close()
+ t = median(times)
+ return t, times
+
+def time_coef(tc, nc, tb, nb):
+ """Return time coefficient relative to base numbers.
+ @param tc: current test time
+ @param nc: current test data size
+ @param tb: base test time
+ @param nb: base test data size
+ @return: time coef.
+ """
+ tc = float(tc)
+ nc = float(nc)
+ tb = float(tb)
+ nb = float(nb)
+ q = (tc * nb) / (tb * nc)
+ return q
+
+def get_test_data(n1, offset, n2):
+ """Create test data on given pattern.
+ @param n1: size of first part of array at base address 0.
+ @param offset: offset for second part of array.
+ @param n2: size of second part of array at given offset.
+ @return: (overall size, hex file, IntelHex object)
+ """
+ # make IntelHex object
+ ih = intelhex.IntelHex()
+ addr = 0
+ for i in range_g(n1):
+ ih[addr] = addr % 256
+ addr += 1
+ addr += offset
+ for i in range_g(n2):
+ ih[addr] = addr % 256
+ addr += 1
+ # make hex file
+ sio = StringIO()
+ ih.write_hex_file(sio)
+ hexstr = sio.getvalue()
+ sio.close()
+ #
+ return n1+n2, hexstr, ih
+
+def get_base_50K():
+ return get_test_data(50000, 0, 0)
+
+def get_250K():
+ return get_test_data(250000, 0, 0)
+
+def get_100K_100K():
+ return get_test_data(100000, 1000000, 100000)
+
+def get_0_100K():
+ return get_test_data(0, 1000000, 100000)
+
+def get_1M():
+ return get_test_data(1000000, 0, 0)
+
+
+class Measure(object):
+ """Measure execution time helper."""
+
+ data_set = [
+ # (data name, getter)
+ ('base 50K', get_base_50K), # first should be base numbers
+ ('250K', get_250K),
+ ('1M', get_1M),
+ ('100K+100K', get_100K_100K),
+ ('0+100K', get_0_100K),
+ ]
+
+ def __init__(self, n=3, read=True, write=True):
+ self.n = n
+ self.read = read
+ self.write = write
+ self.results = []
+
+ def measure_one(self, data):
+ """Do measuring of read and write operations.
+ @param data: 3-tuple from get_test_data
+ @return: (time readhex, time writehex)
+ """
+ _unused, hexstr, ih = data
+ tread, twrite = 0.0, 0.0
+ if self.read:
+ tread = run_readtest_N_times(intelhex.IntelHex, hexstr, self.n)[0]
+ if self.write:
+ twrite = run_writetest_N_times(ih.write_hex_file, self.n)[0]
+ return tread, twrite
+
+ def measure_all(self):
+ for name, getter in self.data_set:
+ data = getter()
+ times = self.measure_one(data)
+ self.results.append((name, times, data[0]))
+
+ def print_report(self, to_file=None):
+ if to_file is None:
+ to_file = sys.stdout
+
+ base_title, base_times, base_n = self.results[0]
+ base_read, base_write = base_times
+ read_report = ['%-10s\t%7.3f' % (base_title, base_read)]
+ write_report = ['%-10s\t%7.3f' % (base_title, base_write)]
+
+ for item in self.results[1:]:
+ cur_title, cur_times, cur_n = item
+ cur_read, cur_write = cur_times
+ if self.read:
+ qread = time_coef(cur_read, cur_n,
+ base_read, base_n)
+ read_report.append('%-10s\t%7.3f\t%7.3f' % (cur_title,
+ cur_read,
+ qread))
+ if self.write:
+ qwrite = time_coef(cur_write, cur_n,
+ base_write, base_n)
+ write_report.append('%-10s\t%7.3f\t%7.3f' % (cur_title,
+ cur_write,
+ qwrite))
+ if self.read:
+ to_file.write('Read operation:\n')
+ to_file.write('\n'.join(read_report))
+ to_file.write('\n\n')
+ if self.write:
+ to_file.write('Write operation:\n')
+ to_file.write('\n'.join(write_report))
+ to_file.write('\n\n')
+
+
+HELP = """\
+Usage: python _bench.py [OPTIONS]
+
+Options:
+ -h this help
+ -n N repeat tests N times
+ -r run only tests for read operation
+ -w run only tests for write operation
+
+If option -r or -w is not specified then all tests will be run.
+"""
+
+
+def main(argv=None):
+ """Main function to run benchmarks.
+ @param argv: command-line arguments.
+ @return: exit code (0 is OK).
+ """
+ import getopt
+
+ # default values
+ test_read = None
+ test_write = None
+ n = 3 # number of repeat
+
+ if argv is None:
+ argv = sys.argv[1:]
+
+ try:
+ opts, args = getopt.getopt(argv, 'hn:rw', [])
+
+ for o,a in opts:
+ if o == '-h':
+ print(HELP)
+ return 0
+ elif o == '-n':
+ n = int(a)
+ elif o == '-r':
+ test_read = True
+ elif o == '-w':
+ test_write = True
+
+ if args:
+ raise getopt.GetoptError('Arguments are not used.')
+ except getopt.GetoptError:
+ msg = sys.exc_info()[1] # current exception
+ txt = str(msg)
+ print(txt)
+ return 1
+
+ if (test_read, test_write) == (None, None):
+ test_read = test_write = True
+
+ m = Measure(n, test_read, test_write)
+ m.measure_all()
+ m.print_report()
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
+
+
+"""
+
+Some Results
+************
+
+
+21/04/2007 revno.40
+Python 2.5 @ Windows XP, Intel Celeron M CPU 430 @ 1.73GHz
+
+Read operation:
+base 10K 0.031
+100K 0.360 1.161
+1M 3.500 1.129
+100K+100K 0.719 1.160
+0+100K 0.360 1.161
+
+Write operation:
+base 10K 0.031
+100K 0.297 0.958
+1M 2.953 0.953
+100K+100K 1.328 2.142
+0+100K 0.312 1.006
+
+
+21/04/2007 revno.46
+Python 2.5 @ Windows XP, Intel Celeron M CPU 430 @ 1.73GHz
+
+Read operation:
+base 10K 0.016
+100K 0.203 1.269
+1M 2.000 1.250
+100K+100K 0.422 1.319
+0+100K 0.203 1.269
+
+Write operation:
+base 10K 0.031
+100K 0.297 0.958
+1M 2.969 0.958
+100K+100K 1.328 2.142
+0+100K 0.312 1.006
+
+
+22/04/2007 revno.48
+Python 2.5 @ Windows XP, Intel Celeron M CPU 430 @ 1.73GHz
+
+Read operation:
+base 10K 0.016
+100K 0.187 1.169
+1M 1.891 1.182
+100K+100K 0.406 1.269
+0+100K 0.188 1.175
+
+Write operation:
+base 10K 0.031
+100K 0.296 0.955
+1M 2.969 0.958
+100K+100K 1.328 2.142
+0+100K 0.312 1.006
+
+
+19/08/2008 revno.72
+Python 2.5.2 @ Windows XP, Intel Celeron M CPU 430 @ 1.73GHz
+
+Read operation:
+base 10K 0.016
+100K 0.171 1.069
+1M 1.734 1.084
+100K+100K 0.375 1.172
+0+100K 0.172 1.075
+
+Write operation:
+base 10K 0.016
+100K 0.156 0.975
+1M 1.532 0.957
+100K+100K 0.344 1.075
+0+100K 0.156 0.975
+
+"""
diff --git a/mixly/tools/python/intelhex/compat.py b/mixly/tools/python/intelhex/compat.py
new file mode 100644
index 00000000..2a6bee6c
--- /dev/null
+++ b/mixly/tools/python/intelhex/compat.py
@@ -0,0 +1,160 @@
+# Copyright (c) 2011, Bernhard Leiner
+# Copyright (c) 2013-2018 Alexander Belchenko
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms,
+# with or without modification, are permitted provided
+# that the following conditions are met:
+#
+# * Redistributions of source code must retain
+# the above copyright notice, this list of conditions
+# and the following disclaimer.
+# * Redistributions in binary form must reproduce
+# the above copyright notice, this list of conditions
+# and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name of the author nor the names
+# of its contributors may be used to endorse
+# or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''Compatibility functions for python 2 and 3.
+
+@author Bernhard Leiner (bleiner AT gmail com)
+@author Alexander Belchenko (alexander belchenko AT gmail com)
+'''
+
+__docformat__ = "javadoc"
+
+
+import sys, array
+
+
+if sys.version_info[0] >= 3:
+ # Python 3
+ Python = 3
+
+ def asbytes(s):
+ if isinstance(s, bytes):
+ return s
+ return s.encode('latin1')
+ def asstr(s):
+ if isinstance(s, str):
+ return s
+ return s.decode('latin1')
+
+ # for python >= 3.2 use 'tobytes', otherwise 'tostring'
+ array_tobytes = array.array.tobytes if sys.version_info[1] >= 2 else array.array.tostring
+
+ IntTypes = (int,)
+ StrType = str
+ UnicodeType = str
+
+ range_g = range # range generator
+ def range_l(*args): # range list
+ return list(range(*args))
+
+ def dict_keys(dikt): # dict keys list
+ return list(dikt.keys())
+ def dict_keys_g(dikt): # dict keys generator
+ return dikt.keys()
+ def dict_items_g(dikt): # dict items generator
+ return dikt.items()
+
+ from io import StringIO, BytesIO
+
+ def get_binary_stdout():
+ return sys.stdout.buffer
+
+ def get_binary_stdin():
+ return sys.stdin.buffer
+
+else:
+ # Python 2
+ Python = 2
+
+ asbytes = str
+ asstr = str
+
+ array_tobytes = array.array.tostring
+
+ IntTypes = (int, long)
+ StrType = basestring
+ UnicodeType = unicode
+
+ #range_g = xrange # range generator
+ def range_g(*args):
+ # we want to use xrange here but on python 2 it does not work with long ints
+ try:
+ return xrange(*args)
+ except OverflowError:
+ start = 0
+ stop = 0
+ step = 1
+ n = len(args)
+ if n == 1:
+ stop = args[0]
+ elif n == 2:
+ start, stop = args
+ elif n == 3:
+ start, stop, step = args
+ else:
+ raise TypeError('wrong number of arguments in range_g call!')
+ if step == 0:
+ raise ValueError('step cannot be zero')
+ if step > 0:
+ def up(start, stop, step):
+ while start < stop:
+ yield start
+ start += step
+ return up(start, stop, step)
+ else:
+ def down(start, stop, step):
+ while start > stop:
+ yield start
+ start += step
+ return down(start, stop, step)
+
+ range_l = range # range list
+
+ def dict_keys(dikt): # dict keys list
+ return dikt.keys()
+ def dict_keys_g(dikt): # dict keys generator
+ return dikt.keys()
+ def dict_items_g(dikt): # dict items generator
+ return dikt.items()
+
+ from cStringIO import StringIO
+ BytesIO = StringIO
+
+ import os
+ def _force_stream_binary(stream):
+ """Force binary mode for stream on Windows."""
+ if os.name == 'nt':
+ f_fileno = getattr(stream, 'fileno', None)
+ if f_fileno:
+ fileno = f_fileno()
+ if fileno >= 0:
+ import msvcrt
+ msvcrt.setmode(fileno, os.O_BINARY)
+ return stream
+
+ def get_binary_stdout():
+ return _force_stream_binary(sys.stdout)
+
+ def get_binary_stdin():
+ return _force_stream_binary(sys.stdin)
diff --git a/mixly/tools/python/intelhex/getsizeof.py b/mixly/tools/python/intelhex/getsizeof.py
new file mode 100644
index 00000000..b91d7ebc
--- /dev/null
+++ b/mixly/tools/python/intelhex/getsizeof.py
@@ -0,0 +1,64 @@
+# Recursive version sys.getsizeof(). Extendable with custom handlers.
+# Code from http://code.activestate.com/recipes/577504/
+# Created by Raymond Hettinger on Fri, 17 Dec 2010 (MIT)
+
+import sys
+from itertools import chain
+from collections import deque
+try:
+ from reprlib import repr
+except ImportError:
+ pass
+
+def total_size(o, handlers={}, verbose=False):
+ """ Returns the approximate memory footprint an object and all of its contents.
+
+ Automatically finds the contents of the following builtin containers and
+ their subclasses: tuple, list, deque, dict, set and frozenset.
+ To search other containers, add handlers to iterate over their contents:
+
+ handlers = {SomeContainerClass: iter,
+ OtherContainerClass: OtherContainerClass.get_elements}
+
+ """
+ dict_handler = lambda d: chain.from_iterable(d.items())
+ all_handlers = {tuple: iter,
+ list: iter,
+ deque: iter,
+ dict: dict_handler,
+ set: iter,
+ frozenset: iter,
+ }
+ all_handlers.update(handlers) # user handlers take precedence
+ seen = set() # track which object id's have already been seen
+ default_size = sys.getsizeof(0) # estimate sizeof object without __sizeof__
+
+ def sizeof(o):
+ if id(o) in seen: # do not double count the same object
+ return 0
+ seen.add(id(o))
+ s = sys.getsizeof(o, default_size)
+
+ if verbose:
+ print(s, type(o), repr(o))#, file=stderr)
+
+ for typ, handler in all_handlers.items():
+ if isinstance(o, typ):
+ s += sum(map(sizeof, handler(o)))
+ break
+ return s
+
+ return sizeof(o)
+
+
+##### Example call #####
+
+if __name__ == '__main__':
+ #d = dict(a=1, b=2, c=3, d=[4,5,6,7], e='a string of chars')
+ print("dict 3 elements")
+ d = {0:0xFF, 1:0xEE, 2:0xCC}
+ print(total_size(d, verbose=True))
+
+ #print("array 3 elements")
+ #import array
+ #print(total_size(array.array('B', b'\x01\x02\x03')))
diff --git a/mixly/tools/python/intelhex/test.py b/mixly/tools/python/intelhex/test.py
new file mode 100644
index 00000000..50276b0a
--- /dev/null
+++ b/mixly/tools/python/intelhex/test.py
@@ -0,0 +1,1799 @@
+# Copyright (c) 2005-2018, Alexander Belchenko
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms,
+# with or without modification, are permitted provided
+# that the following conditions are met:
+#
+# * Redistributions of source code must retain
+# the above copyright notice, this list of conditions
+# and the following disclaimer.
+# * Redistributions in binary form must reproduce
+# the above copyright notice, this list of conditions
+# and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name of the author nor the names
+# of its contributors may be used to endorse
+# or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Test suite for IntelHex library."""
+
+import array
+import os
+import shlex
+import subprocess
+import sys
+import tempfile
+import unittest
+
+import intelhex
+from intelhex import (
+ IntelHex,
+ IntelHexError,
+ HexReaderError,
+ AddressOverlapError,
+ HexRecordError,
+ RecordLengthError,
+ RecordTypeError,
+ RecordChecksumError,
+ EOFRecordError,
+ ExtendedSegmentAddressRecordError,
+ ExtendedLinearAddressRecordError,
+ StartSegmentAddressRecordError,
+ StartLinearAddressRecordError,
+ DuplicateStartAddressRecordError,
+ InvalidStartAddressValueError,
+ _EndOfFile,
+ BadAccess16bit,
+ hex2bin,
+ Record,
+ )
+from intelhex import compat
+from intelhex.compat import (
+ BytesIO,
+ StringIO,
+ UnicodeType,
+ array_tobytes,
+ asbytes,
+ asstr,
+ dict_items_g,
+ range_g,
+ range_l,
+ )
+from intelhex.__version__ import version_str
+
+__docformat__ = 'restructuredtext'
+
+##
+# Data for tests
+
+hex8 = '''\
+:1004E300CFF0FBE2FDF220FF20F2E120E2FBE6F396
+:1004F3000A00FDE0E1E2E3B4E4E5BAE6E7B3BFE80E
+:10050300E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8E0
+:10051300F9FCFEFF00C0C1C2C3A5C4C5AAC6C7B2C9
+:10052300AFC8C9CACBCCCDCECFD0D1D2D3D4D5D6F8
+:07053300D7D8D9DCDEDF00A0
+:10053A0078227C007D007BFF7A0479F57E007F2398
+:10054A0012042F78457C007D007BFF7A0579187E9E
+:10055A00007F2212042F759850438920758DDDD2B1
+:10056A008ED2996390017BFF7A0479E31200658049
+:01057A00FE82
+:030000000205A254
+:0C05A200787FE4F6D8FD75817A02053AF6
+:10035F00E709F608DFFA8046E709F208DFFA803E80
+:10036F0088828C83E709F0A3DFFA8032E309F6086D
+:10037F00DFFA8078E309F208DFFA807088828C83D5
+:10038F00E309F0A3DFFA806489828A83E0A3F60889
+:10039F00DFFA805889828A83E0A3F208DFFA804C63
+:1003AF0080D280FA80C680D4806980F2803380103A
+:1003BF0080A680EA809A80A880DA80E280CA8033A3
+:1003CF0089828A83ECFAE493A3C8C582C8CCC5831B
+:1003DF00CCF0A3C8C582C8CCC583CCDFE9DEE780EB
+:1003EF000D89828A83E493A3F608DFF9ECFAA9F06A
+:1003FF00EDFB2289828A83ECFAE0A3C8C582C8CCC0
+:10040F00C583CCF0A3C8C582C8CCC583CCDFEADED8
+:10041F00E880DB89828A83E493A3F208DFF980CC3A
+:10042F0088F0EF60010E4E60C388F0ED2402B40433
+:10043F000050B9F582EB2402B4040050AF232345DA
+:06044F0082239003AF734D
+:10000300E576246AF8E60576227867300702786A8F
+:10001300E475F0011204AD0204552000EB7F2ED2EB
+:10002300008018EF540F2490D43440D4FF30040BD5
+:10003300EF24BFB41A0050032461FFE57760021573
+:1000430077057AE57A7002057930070D7867E475EC
+:10005300F0011204ADEF02049B02057B7403D20787
+:100063008003E4C207F5768B678A688969E4F577CC
+:10007300F579F57AE57760077F2012003E80F57504
+:1000830078FFC201C200C202C203C205C206C2088F
+:1000930012000CFF700D3007057F0012004FAF7A7E
+:1000A300AE7922B4255FC2D5C20412000CFF24D05E
+:1000B300B40A00501A75F00A787730D50508B6FFF0
+:1000C3000106C6A426F620D5047002D20380D924E3
+:1000D300CFB41A00EF5004C2E5D20402024FD2019A
+:1000E30080C6D20080C0D20280BCD2D580BAD205ED
+:1000F30080B47F2012003E2002077401B5770040D0
+:10010300F1120003FF12003E020077D208D20680EC
+:1001130095120003FB120003FA120003F94A4B7015
+:100123000679207A037BFF20022EE577602A7E0082
+:100133008E8275830012046E60060EEE657870F091
+:10014300C2D5EBC0E0EAC0E0E9C0E0EE120296D00F
+:10015300E0F9D0E0FAD0E0FB120455FF60AAEBC04F
+:10016300E0EAC0E0E9C0E012003ED0E02401F9D0AB
+:10017300E03400FAD0E0FBE5780460DCD578D98080
+:10018300877BFF7A027992D202809C791080027970
+:1001930008C206C2088008D2D5790A8004790AC247
+:1001A300D5E578047002F578E4FAFDFEFF1200034A
+:1001B300FC7B08200113120003FD7B1030000A12A0
+:1001C3000003FE120003FF7B20EC3382D592D5504F
+:1001D30013C3E43000069FFFE49EFEE42001039D69
+:1001E300FDE49CFCE4CBF8C201EC700CCFCECDCC8B
+:1001F300E824F8F870F38017C3EF33FFEE33FEED16
+:1002030033FDEC33FCEB33FB994002FB0FD8E9EBF6
+:10021300300105F8D0E0C448B201C0E00AEC4D4E0D
+:100223004F78207B0070C2EAB5780040BCC0E01272
+:100233000298D0F0D0E0200104C4C0E0C4B201C0F1
+:10024300F0120027D0F0D5F0EB0200771204BD01C5
+:100253001453018E5800E54C00E14201924F019A7C
+:0F02630044019A4900FA4301A0550184460184E1
+:100272004501844703405000E92D00ED2E01102B6B
+:1002820000F123010E2003292A00A94800000108D9
+:100292003F3F3F00790AA2D5200314300509B91067
+:1002A200020404B9080104A2D52006025001042068
+:1002B20002689202B577005034C0E07F2030031903
+:1002C2007F30A20272067205500F1202EFC202C202
+:1002D20006C205C2087F30800F300503E9C0E01274
+:1002E200003E300503D0E0F9D0E0B577CC300517F9
+:1002F2007F30B9100C12003E7F583004077F78809F
+:1003020003B9080312003E3002057F2D02003E7F32
+:10031200202008F87F2B2006F322920280CF286E3D
+:10032200756C6C2900D2011200033001F8C2017809
+:100332007730D50108F60200A92D50434958120022
+:10034200032403B405004001E490033B9312002F01
+:0D035200743A12002FD20375770402018E59
+:10045500BB010689828A83E0225002E722BBFE02A5
+:09046500E32289828A83E49322D8
+:10046E00BB010CE58229F582E5833AF583E0225043
+:10047E0006E92582F8E622BBFE06E92582F8E2228D
+:0D048E00E58229F582E5833AF583E49322A7
+:10049B00BB010689828A83F0225002F722BBFE0140
+:0204AB00F3223A
+:1004AD00FAE6FB0808E6F925F0F618E6CA3AF62250
+:1004BD00D083D082F8E4937012740193700DA3A3CE
+:1004CD0093F8740193F5828883E4737402936860E2
+:0604DD00EFA3A3A380DFE2
+:10057B00EFB40A07740D120586740A309811A89906
+:10058B00B8130CC2983098FDA899C298B811F630E0
+:07059B0099FDC299F59922B8
+:00000001FF
+'''
+bin8 = array.array('B',[2, 5, 162, 229, 118, 36, 106, 248, 230, 5, 118, 34,
+ 120, 103, 48, 7, 2, 120, 106, 228, 117, 240, 1, 18,
+ 4, 173, 2, 4, 85, 32, 0, 235, 127, 46, 210, 0, 128,
+ 24, 239, 84, 15, 36, 144, 212, 52, 64, 212, 255, 48,
+ 4, 11, 239, 36, 191, 180, 26, 0, 80, 3, 36, 97, 255,
+ 229, 119, 96, 2, 21, 119, 5, 122, 229, 122, 112, 2,
+ 5, 121, 48, 7, 13, 120, 103, 228, 117, 240, 1, 18,
+ 4, 173, 239, 2, 4, 155, 2, 5, 123, 116, 3, 210, 7,
+ 128, 3, 228, 194, 7, 245, 118, 139, 103, 138, 104,
+ 137, 105, 228, 245, 119, 245, 121, 245, 122, 229,
+ 119, 96, 7, 127, 32, 18, 0, 62, 128, 245, 117, 120,
+ 255, 194, 1, 194, 0, 194, 2, 194, 3, 194, 5, 194, 6,
+ 194, 8, 18, 0, 12, 255, 112, 13, 48, 7, 5, 127, 0,
+ 18, 0, 79, 175, 122, 174, 121, 34, 180, 37, 95, 194,
+ 213, 194, 4, 18, 0, 12, 255, 36, 208, 180, 10, 0, 80,
+ 26, 117, 240, 10, 120, 119, 48, 213, 5, 8, 182, 255,
+ 1, 6, 198, 164, 38, 246, 32, 213, 4, 112, 2, 210, 3,
+ 128, 217, 36, 207, 180, 26, 0, 239, 80, 4, 194, 229,
+ 210, 4, 2, 2, 79, 210, 1, 128, 198, 210, 0, 128, 192,
+ 210, 2, 128, 188, 210, 213, 128, 186, 210, 5, 128,
+ 180, 127, 32, 18, 0, 62, 32, 2, 7, 116, 1, 181, 119,
+ 0, 64, 241, 18, 0, 3, 255, 18, 0, 62, 2, 0, 119, 210,
+ 8, 210, 6, 128, 149, 18, 0, 3, 251, 18, 0, 3, 250,
+ 18, 0, 3, 249, 74, 75, 112, 6, 121, 32, 122, 3, 123,
+ 255, 32, 2, 46, 229, 119, 96, 42, 126, 0, 142, 130,
+ 117, 131, 0, 18, 4, 110, 96, 6, 14, 238, 101, 120,
+ 112, 240, 194, 213, 235, 192, 224, 234, 192, 224,
+ 233, 192, 224, 238, 18, 2, 150, 208, 224, 249, 208,
+ 224, 250, 208, 224, 251, 18, 4, 85, 255, 96, 170,
+ 235, 192, 224, 234, 192, 224, 233, 192, 224, 18, 0,
+ 62, 208, 224, 36, 1, 249, 208, 224, 52, 0, 250, 208,
+ 224, 251, 229, 120, 4, 96, 220, 213, 120, 217, 128,
+ 135, 123, 255, 122, 2, 121, 146, 210, 2, 128, 156,
+ 121, 16, 128, 2, 121, 8, 194, 6, 194, 8, 128, 8, 210,
+ 213, 121, 10, 128, 4, 121, 10, 194, 213, 229, 120, 4,
+ 112, 2, 245, 120, 228, 250, 253, 254, 255, 18, 0, 3,
+ 252, 123, 8, 32, 1, 19, 18, 0, 3, 253, 123, 16, 48,
+ 0, 10, 18, 0, 3, 254, 18, 0, 3, 255, 123, 32, 236,
+ 51, 130, 213, 146, 213, 80, 19, 195, 228, 48, 0, 6,
+ 159, 255, 228, 158, 254, 228, 32, 1, 3, 157, 253,
+ 228, 156, 252, 228, 203, 248, 194, 1, 236, 112, 12,
+ 207, 206, 205, 204, 232, 36, 248, 248, 112, 243, 128,
+ 23, 195, 239, 51, 255, 238, 51, 254, 237, 51, 253,
+ 236, 51, 252, 235, 51, 251, 153, 64, 2, 251, 15, 216,
+ 233, 235, 48, 1, 5, 248, 208, 224, 196, 72, 178, 1,
+ 192, 224, 10, 236, 77, 78, 79, 120, 32, 123, 0, 112,
+ 194, 234, 181, 120, 0, 64, 188, 192, 224, 18, 2, 152,
+ 208, 240, 208, 224, 32, 1, 4, 196, 192, 224, 196,
+ 178, 1, 192, 240, 18, 0, 39, 208, 240, 213, 240, 235,
+ 2, 0, 119, 18, 4, 189, 1, 20, 83, 1, 142, 88, 0, 229,
+ 76, 0, 225, 66, 1, 146, 79, 1, 154, 68, 1, 154, 73,
+ 0, 250, 67, 1, 160, 85, 1, 132, 70, 1, 132, 69, 1,
+ 132, 71, 3, 64, 80, 0, 233, 45, 0, 237, 46, 1, 16,
+ 43, 0, 241, 35, 1, 14, 32, 3, 41, 42, 0, 169, 72, 0,
+ 0, 1, 8, 63, 63, 63, 0, 121, 10, 162, 213, 32, 3, 20,
+ 48, 5, 9, 185, 16, 2, 4, 4, 185, 8, 1, 4, 162, 213,
+ 32, 6, 2, 80, 1, 4, 32, 2, 104, 146, 2, 181, 119, 0,
+ 80, 52, 192, 224, 127, 32, 48, 3, 25, 127, 48, 162,
+ 2, 114, 6, 114, 5, 80, 15, 18, 2, 239, 194, 2, 194,
+ 6, 194, 5, 194, 8, 127, 48, 128, 15, 48, 5, 3, 233,
+ 192, 224, 18, 0, 62, 48, 5, 3, 208, 224, 249, 208,
+ 224, 181, 119, 204, 48, 5, 23, 127, 48, 185, 16, 12,
+ 18, 0, 62, 127, 88, 48, 4, 7, 127, 120, 128, 3, 185,
+ 8, 3, 18, 0, 62, 48, 2, 5, 127, 45, 2, 0, 62, 127,
+ 32, 32, 8, 248, 127, 43, 32, 6, 243, 34, 146, 2, 128,
+ 207, 40, 110, 117, 108, 108, 41, 0, 210, 1, 18, 0, 3,
+ 48, 1, 248, 194, 1, 120, 119, 48, 213, 1, 8, 246, 2,
+ 0, 169, 45, 80, 67, 73, 88, 18, 0, 3, 36, 3, 180, 5,
+ 0, 64, 1, 228, 144, 3, 59, 147, 18, 0, 47, 116, 58,
+ 18, 0, 47, 210, 3, 117, 119, 4, 2, 1, 142, 231, 9,
+ 246, 8, 223, 250, 128, 70, 231, 9, 242, 8, 223, 250,
+ 128, 62, 136, 130, 140, 131, 231, 9, 240, 163, 223,
+ 250, 128, 50, 227, 9, 246, 8, 223, 250, 128, 120,
+ 227, 9, 242, 8, 223, 250, 128, 112, 136, 130, 140,
+ 131, 227, 9, 240, 163, 223, 250, 128, 100, 137,
+ 130, 138, 131, 224, 163, 246, 8, 223, 250, 128, 88,
+ 137, 130, 138, 131, 224, 163, 242, 8, 223, 250, 128,
+ 76, 128, 210, 128, 250, 128, 198, 128, 212, 128, 105,
+ 128, 242, 128, 51, 128, 16, 128, 166, 128, 234, 128,
+ 154, 128, 168, 128, 218, 128, 226, 128, 202, 128, 51,
+ 137, 130, 138, 131, 236, 250, 228, 147, 163, 200,
+ 197, 130, 200, 204, 197, 131, 204, 240, 163, 200,
+ 197, 130, 200, 204, 197, 131, 204, 223, 233, 222,
+ 231, 128, 13, 137, 130, 138, 131, 228, 147, 163, 246,
+ 8, 223, 249, 236, 250, 169, 240, 237, 251, 34, 137,
+ 130, 138, 131, 236, 250, 224, 163, 200, 197, 130,
+ 200, 204, 197, 131, 204, 240, 163, 200, 197, 130,
+ 200, 204, 197, 131, 204, 223, 234, 222, 232, 128,
+ 219, 137, 130, 138, 131, 228, 147, 163, 242, 8,
+ 223, 249, 128, 204, 136, 240, 239, 96, 1, 14, 78,
+ 96, 195, 136, 240, 237, 36, 2, 180, 4, 0, 80, 185,
+ 245, 130, 235, 36, 2, 180, 4, 0, 80, 175, 35, 35,
+ 69, 130, 35, 144, 3, 175, 115, 187, 1, 6, 137, 130,
+ 138, 131, 224, 34, 80, 2, 231, 34, 187, 254, 2, 227,
+ 34, 137, 130, 138, 131, 228, 147, 34, 187, 1, 12,
+ 229, 130, 41, 245, 130, 229, 131, 58, 245, 131, 224,
+ 34, 80, 6, 233, 37, 130, 248, 230, 34, 187, 254, 6,
+ 233, 37, 130, 248, 226, 34, 229, 130, 41, 245, 130,
+ 229, 131, 58, 245, 131, 228, 147, 34, 187, 1, 6,
+ 137, 130, 138, 131, 240, 34, 80, 2, 247, 34, 187,
+ 254, 1, 243, 34, 250, 230, 251, 8, 8, 230, 249, 37,
+ 240, 246, 24, 230, 202, 58, 246, 34, 208, 131, 208,
+ 130, 248, 228, 147, 112, 18, 116, 1, 147, 112, 13,
+ 163, 163, 147, 248, 116, 1, 147, 245, 130, 136,
+ 131, 228, 115, 116, 2, 147, 104, 96, 239, 163, 163,
+ 163, 128, 223, 207, 240, 251, 226, 253, 242, 32,
+ 255, 32, 242, 225, 32, 226, 251, 230, 243, 10, 0,
+ 253, 224, 225, 226, 227, 180, 228, 229, 186, 230,
+ 231, 179, 191, 232, 233, 234, 235, 236, 237, 238,
+ 239, 240, 241, 242, 243, 244, 245, 246, 247, 248,
+ 249, 252, 254, 255, 0, 192, 193, 194, 195, 165, 196,
+ 197, 170, 198, 199, 178, 175, 200, 201, 202, 203,
+ 204, 205, 206, 207, 208, 209, 210, 211, 212, 213,
+ 214, 215, 216, 217, 220, 222, 223, 0, 120, 34, 124,
+ 0, 125, 0, 123, 255, 122, 4, 121, 245, 126, 0, 127,
+ 35, 18, 4, 47, 120, 69, 124, 0, 125, 0, 123, 255,
+ 122, 5, 121, 24, 126, 0, 127, 34, 18, 4, 47, 117,
+ 152, 80, 67, 137, 32, 117, 141, 221, 210, 142, 210,
+ 153, 99, 144, 1, 123, 255, 122, 4, 121, 227, 18, 0,
+ 101, 128, 254, 239, 180, 10, 7, 116, 13, 18, 5, 134,
+ 116, 10, 48, 152, 17, 168, 153, 184, 19, 12, 194,
+ 152, 48, 152, 253, 168, 153, 194, 152, 184, 17,
+ 246, 48, 153, 253, 194, 153, 245, 153, 34, 120, 127,
+ 228, 246, 216, 253, 117, 129, 122, 2, 5, 58])
+
+
+hex16 = """:020000040000FA
+:10000000000083120313072055301820042883169C
+:10001000031340309900181598168312031318160D
+:1000200098170800831203138C1E14281A0808005E
+:0C003000831203130C1E1A28990008000C
+:00000001FF
+"""
+bin16 = array.array('H', [0x0000, 0x1283, 0x1303, 0x2007,
+ 0x3055, 0x2018, 0x2804, 0x1683,
+ 0x1303, 0x3040, 0x0099, 0x1518,
+ 0x1698, 0x1283, 0x1303, 0x1618,
+ 0x1798, 0x0008, 0x1283, 0x1303,
+ 0x1E8C, 0x2814, 0x081A, 0x0008,
+ 0x1283, 0x1303, 0x1E0C, 0x281A,
+ 0x0099, 0x0008, 0x3FFF, 0x3FFF])
+
+
+hex64k = """:020000040000FA
+:0100000001FE
+:020000040001F9
+:0100000002FD
+:00000001FF
+"""
+data64k = {0: 1, 0x10000: 2}
+
+
+hex_rectype3 = """:0400000312345678E5
+:0100000001FE
+:00000001FF
+"""
+data_rectype3 = {0: 1}
+start_addr_rectype3 = {'CS': 0x1234, 'IP': 0x5678}
+
+
+hex_rectype5 = """:0400000512345678E3
+:0100000002FD
+:00000001FF
+"""
+data_rectype5 = {0: 2}
+start_addr_rectype5 = {'EIP': 0x12345678}
+
+hex_empty_file = ':00000001FF\n'
+
+hex_simple = """\
+:10000000000083120313072055301820042883169C
+:10001000031340309900181598168312031318160D
+:1000200098170800831203138C1E14281A0808005E
+:0C003000831203130C1E1A28990008000C
+:00000001FF
+"""
+
+hex_bug_lp_341051 = """\
+:020FEC00E4E738
+:040FF00022E122E1F7
+:00000001FF
+"""
+
+
+##
+# Test cases
+
+class TestIntelHexBase(unittest.TestCase):
+ """Base class for all tests.
+ Provide additional functionality for testing.
+ """
+
+ def assertRaisesMsg(self, excClass, msg, callableObj, *args, **kwargs):
+ """Just like unittest.TestCase.assertRaises,
+ but checks that the message is right too.
+
+ Borrowed from Ned Batchelder Blog.
+ See: http://www.nedbatchelder.com/blog/200609.html#e20060905T064418
+
+ Typical usage::
+
+ self.assertRaisesMsg(MyException, "Exception message",
+ my_function, (arg1, arg2))
+ """
+ try:
+ callableObj(*args, **kwargs)
+ except excClass:
+ exc = sys.exc_info()[1] # current exception
+ excMsg = str(exc)
+ if not msg:
+ # No message provided: any message is fine.
+ return
+ elif excMsg == msg:
+ # Message provided, and we got the right message: it passes.
+ return
+ else:
+ # Message provided, and it didn't match: fail!
+ raise self.failureException(
+ "Right exception, wrong message: got '%s' expected '%s'" %
+ (excMsg, msg)
+ )
+ else:
+ if hasattr(excClass, '__name__'):
+ excName = excClass.__name__
+ else:
+ excName = str(excClass)
+ raise self.failureException(
+ "Expected to raise %s, didn't get an exception at all" %
+ excName
+ )
+
+ def assertEqualWrittenData(self, a, b):
+ return self.assertEqual(a, b, """Written data is incorrect
+Should be:
+%s
+
+Written:
+%s
+""" % (a, b))
+#/class TestIntelHexBase
+
+
+class TestIntelHex(TestIntelHexBase):
+
+ def setUp(self):
+ self.f = StringIO(hex8)
+
+ def tearDown(self):
+ self.f.close()
+ del self.f
+
+ def test_init_from_file(self):
+ ih = IntelHex(self.f)
+ for addr in range_g(len(bin8)):
+ expected = bin8[addr]
+ actual = ih[addr]
+ self.assertEqual(expected, actual,
+ "Data different at address "
+ "%x (%x != %x)" % (addr, expected, actual))
+
+ def test_hex_fromfile(self):
+ ih = IntelHex()
+ ih.fromfile(self.f, format='hex')
+ for addr in range_g(len(bin8)):
+ expected = bin8[addr]
+ actual = ih[addr]
+ self.assertEqual(expected, actual,
+ "Data different at address "
+ "%x (%x != %x)" % (addr, expected, actual))
+
+ def test_unicode_filename(self):
+ handle, fname = tempfile.mkstemp(UnicodeType(''))
+ os.close(handle)
+ try:
+ self.assertTrue(isinstance(fname, UnicodeType))
+ f = open(fname, 'w')
+ try:
+ f.write(hex8)
+ finally:
+ f.close()
+ ih = IntelHex(fname)
+ self.assertEqual(0, ih.minaddr())
+ self.assertEqual(len(bin8)-1, ih.maxaddr())
+ finally:
+ os.remove(fname)
+
+ def test_tobinarray_empty(self):
+ ih = IntelHex()
+ ih.padding = 0xFF # set-up explicit padding value and don't use pad parameter
+ self.assertEqual(array.array('B', []), ih.tobinarray())
+ self.assertEqual(array.array('B', []), ih.tobinarray(start=0))
+ self.assertEqual(array.array('B', []), ih.tobinarray(end=2))
+ self.assertEqual(array.array('B', [255,255,255]), ih.tobinarray(0,2))
+
+ def test_tobinarray_with_size(self):
+ ih = IntelHex(self.f)
+ self.assertEqual(array.array('B', [2, 5, 162, 229, 118, 36, 106, 248]),
+ ih.tobinarray(size=8)) # from addr 0
+ self.assertEqual(array.array('B', [120, 103, 48, 7, 2, 120, 106, 228]),
+ ih.tobinarray(start=12, size=8))
+ self.assertEqual(array.array('B', [2, 5, 162, 229, 118, 36, 106, 248]),
+ ih.tobinarray(end=7, size=8)) # addr: 0..7, 8 bytes
+ self.assertEqual(array.array('B', [120, 103, 48, 7, 2, 120, 106, 228]),
+ ih.tobinarray(end=19, size=8)) # addr: 12..19, 8 bytes
+ self.assertRaises(ValueError, ih.tobinarray, start=0, end=7, size=8)
+ self.assertRaises(ValueError, ih.tobinarray, end=3, size=8)
+ self.assertRaises(ValueError, ih.tobinarray, size=0)
+ self.assertRaises(ValueError, ih.tobinarray, size=-1)
+
+ def test_tobinstr(self):
+ ih = IntelHex(self.f)
+ s1 = ih.tobinstr()
+ s2 = array_tobytes(bin8)
+ self.assertEqual(s2, s1, "data not equal\n%s\n\n%s" % (s1, s2))
+
+ def test_tobinfile(self):
+ ih = IntelHex(self.f)
+ sio = BytesIO()
+ ih.tobinfile(sio)
+ s1 = sio.getvalue()
+ sio.close()
+ s2 = array_tobytes(bin8)
+ self.assertEqual(s2, s1, "data not equal\n%s\n\n%s" % (s1, s2))
+ # new API: .tofile universal method
+ sio = BytesIO()
+ ih.tofile(sio, format='bin')
+ s1 = sio.getvalue()
+ sio.close()
+ s2 = array_tobytes(bin8)
+ self.assertEqual(s2, s1, "data not equal\n%s\n\n%s" % (s1, s2))
+
+ def test_tobinfile_realfile(self):
+ ih = IntelHex(self.f)
+ tf = tempfile.TemporaryFile(mode='wb')
+ try:
+ ih.tobinfile(tf)
+ finally:
+ tf.close()
+
+ def test__get_eol_textfile(self):
+ self.assertEqual('\n', IntelHex._get_eol_textfile('native', 'win32'))
+ self.assertEqual('\n', IntelHex._get_eol_textfile('native', 'linux'))
+ self.assertEqual('\n', IntelHex._get_eol_textfile('CRLF', 'win32'))
+ self.assertEqual('\r\n', IntelHex._get_eol_textfile('CRLF', 'linux'))
+ self.assertRaisesMsg(ValueError, "wrong eolstyle 'LF'",
+ IntelHex._get_eol_textfile, 'LF', 'win32')
+
+ def test_write_empty_hexfile(self):
+ ih = intelhex.IntelHex()
+ sio = StringIO()
+ ih.write_hex_file(sio)
+ s = sio.getvalue()
+ sio.close()
+ self.assertEqualWrittenData(hex_empty_file, s)
+
+ def test_write_hexfile(self):
+ ih = intelhex.IntelHex(StringIO(hex_simple))
+ sio = StringIO()
+ ih.write_hex_file(sio)
+ s = sio.getvalue()
+ sio.close()
+ self.assertEqualWrittenData(hex_simple, s)
+ # new API: .tofile universal method
+ sio = StringIO()
+ ih.tofile(sio, format='hex')
+ s = sio.getvalue()
+ sio.close()
+ self.assertEqualWrittenData(hex_simple, s)
+
+ def test_write_hex_bug_341051(self):
+ ih = intelhex.IntelHex(StringIO(hex_bug_lp_341051))
+ sio = StringIO()
+ ih.tofile(sio, format='hex')
+ s = sio.getvalue()
+ sio.close()
+ self.assertEqualWrittenData(hex_bug_lp_341051, s)
+
+ def test_write_hex_first_extended_linear_address(self):
+ ih = IntelHex({0x20000: 0x01})
+ sio = StringIO()
+ ih.write_hex_file(sio)
+ s = sio.getvalue()
+ sio.close()
+ # should be
+ r = [Record.extended_linear_address(2),
+ Record.data(0x0000, [0x01]),
+ Record.eof()]
+ h = '\n'.join(r) + '\n'
+ # compare
+ self.assertEqual(h, s)
+
+ def test_tofile_wrong_format(self):
+ ih = IntelHex()
+ sio = StringIO()
+ self.assertRaises(ValueError, ih.tofile, sio, {'format': 'bad'})
+
+ def test_todict(self):
+ ih = IntelHex()
+ self.assertEqual({}, ih.todict())
+ ih = IntelHex(StringIO(hex64k))
+ self.assertEqual(data64k, ih.todict())
+ ih = IntelHex()
+ ih[1] = 2
+ ih.start_addr = {'EIP': 1234}
+ self.assertEqual({1: 2, 'start_addr': {'EIP': 1234}}, ih.todict())
+
+ def test_fromdict(self):
+ ih = IntelHex()
+ ih.fromdict({1:2, 3:4})
+ self.assertEqual({1:2, 3:4}, ih.todict())
+ ih.fromdict({1:5, 6:7})
+ self.assertEqual({1:5, 3:4, 6:7}, ih.todict())
+ ih = IntelHex()
+ ih.fromdict({1: 2, 'start_addr': {'EIP': 1234}})
+ self.assertEqual({1: 2, 'start_addr': {'EIP': 1234}}, ih.todict())
+ # bad dict
+ self.assertRaises(ValueError, ih.fromdict, {'EIP': 1234})
+ self.assertRaises(ValueError, ih.fromdict, {-1: 1234})
+
+ def test_init_from_obj(self):
+ ih = IntelHex({1:2, 3:4})
+ self.assertEqual({1:2, 3:4}, ih.todict())
+ ih.start_addr = {'EIP': 1234}
+ ih2 = IntelHex(ih)
+ ih[1] = 5
+ ih.start_addr = {'EIP': 5678}
+ self.assertEqual({1:2, 3:4, 'start_addr': {'EIP': 1234}}, ih2.todict())
+ self.assertNotEqual(id(ih), id(ih2))
+
+ def test_dict_interface(self):
+ ih = IntelHex()
+ self.assertEqual(0xFF, ih[0]) # padding byte substitution
+ ih[0] = 1
+ self.assertEqual(1, ih[0])
+ del ih[0]
+ self.assertEqual({}, ih.todict()) # padding byte substitution
+
+ def test_len(self):
+ ih = IntelHex()
+ self.assertEqual(0, len(ih))
+ ih[2] = 1
+ self.assertEqual(1, len(ih))
+ ih[1000] = 2
+ self.assertEqual(2, len(ih))
+
+ def test__getitem__(self):
+ ih = IntelHex()
+ # simple cases
+ self.assertEqual(0xFF, ih[0])
+ ih[0] = 1
+ self.assertEqual(1, ih[0])
+ # big address
+ self.assertEqual(0xFF, ih[2**32-1])
+ # wrong addr type/value for indexing operations
+ def getitem(index):
+ return ih[index]
+ self.assertRaisesMsg(TypeError,
+ 'Address should be >= 0.',
+ getitem, -1)
+ self.assertRaisesMsg(TypeError,
+ "Address has unsupported type: %s" % type('foo'),
+ getitem, 'foo')
+ # new object with some data
+ ih = IntelHex()
+ ih[0] = 1
+ ih[1] = 2
+ ih[2] = 3
+ ih[10] = 4
+ # full copy via slicing
+ ih2 = ih[:]
+ self.assertTrue(isinstance(ih2, IntelHex))
+ self.assertEqual({0:1, 1:2, 2:3, 10:4}, ih2.todict())
+ # other slice operations
+ self.assertEqual({}, ih[3:8].todict())
+ self.assertEqual({0:1, 1:2}, ih[0:2].todict())
+ self.assertEqual({0:1, 1:2}, ih[:2].todict())
+ self.assertEqual({2:3, 10:4}, ih[2:].todict())
+ self.assertEqual({0:1, 2:3, 10:4}, ih[::2].todict())
+ self.assertEqual({10:4}, ih[3:11].todict())
+
+ def test__setitem__(self):
+ ih = IntelHex()
+ # simple indexing operation
+ ih[0] = 1
+ self.assertEqual({0:1}, ih.todict())
+ # errors
+ def setitem(a,b):
+ ih[a] = b
+ self.assertRaisesMsg(TypeError,
+ 'Address should be >= 0.',
+ setitem, -1, 0)
+ self.assertRaisesMsg(TypeError,
+ "Address has unsupported type: %s" % type('foo'),
+ setitem, 'foo', 0)
+ # slice operations
+ ih[0:4] = range_l(4)
+ self.assertEqual({0:0, 1:1, 2:2, 3:3}, ih.todict())
+ ih[0:] = range_l(5,9)
+ self.assertEqual({0:5, 1:6, 2:7, 3:8}, ih.todict())
+ ih[:4] = range_l(9,13)
+ self.assertEqual({0:9, 1:10, 2:11, 3:12}, ih.todict())
+ # with step
+ ih = IntelHex()
+ ih[0:8:2] = range_l(4)
+ self.assertEqual({0:0, 2:1, 4:2, 6:3}, ih.todict())
+ # errors in slice operations
+ # ih[1:2] = 'a'
+ self.assertRaisesMsg(ValueError,
+ 'Slice operation expects sequence of bytes',
+ setitem, slice(1,2,None), 'a')
+ # ih[0:1] = [1,2,3]
+ self.assertRaisesMsg(ValueError,
+ 'Length of bytes sequence does not match address range',
+ setitem, slice(0,1,None), [1,2,3])
+ # ih[:] = [1,2,3]
+ self.assertRaisesMsg(TypeError,
+ 'Unsupported address range',
+ setitem, slice(None,None,None), [1,2,3])
+ # ih[:2] = [1,2,3]
+ self.assertRaisesMsg(TypeError,
+ 'start address cannot be negative',
+ setitem, slice(None,2,None), [1,2,3])
+ # ih[0:-3:-1] = [1,2,3]
+ self.assertRaisesMsg(TypeError,
+ 'stop address cannot be negative',
+ setitem, slice(0,-3,-1), [1,2,3])
+
+ def test__delitem__(self):
+ ih = IntelHex()
+ ih[0] = 1
+ del ih[0]
+ self.assertEqual({}, ih.todict())
+ # errors
+ def delitem(addr):
+ del ih[addr]
+ self.assertRaises(KeyError, delitem, 1)
+ self.assertRaisesMsg(TypeError,
+ 'Address should be >= 0.',
+ delitem, -1)
+ self.assertRaisesMsg(TypeError,
+ "Address has unsupported type: %s" % type('foo'),
+ delitem, 'foo')
+ # deleting slice
+ del ih[0:1] # no error here because of slicing
+ #
+ def ihex(size=8):
+ ih = IntelHex()
+ for i in range_g(size):
+ ih[i] = i
+ return ih
+ ih = ihex(8)
+ del ih[:] # delete all data
+ self.assertEqual({}, ih.todict())
+ ih = ihex(8)
+ del ih[2:6]
+ self.assertEqual({0:0, 1:1, 6:6, 7:7}, ih.todict())
+ ih = ihex(8)
+ del ih[::2]
+ self.assertEqual({1:1, 3:3, 5:5, 7:7}, ih.todict())
+
+ def test_addresses(self):
+ # empty object
+ ih = IntelHex()
+ self.assertEqual([], ih.addresses())
+ self.assertEqual(None, ih.minaddr())
+ self.assertEqual(None, ih.maxaddr())
+ # normal object
+ ih = IntelHex({1:2, 7:8, 10:0})
+ self.assertEqual([1,7,10], ih.addresses())
+ self.assertEqual(1, ih.minaddr())
+ self.assertEqual(10, ih.maxaddr())
+
+ def test__get_start_end(self):
+ # test for private method _get_start_end
+ # for empty object
+ ih = IntelHex()
+ self.assertRaises(intelhex.EmptyIntelHexError, ih._get_start_end)
+ self.assertRaises(intelhex.EmptyIntelHexError, ih._get_start_end, size=10)
+ self.assertEqual((0,9), ih._get_start_end(start=0, size=10))
+ self.assertEqual((1,10), ih._get_start_end(end=10, size=10))
+ # normal object
+ ih = IntelHex({1:2, 7:8, 10:0})
+ self.assertEqual((1,10), ih._get_start_end())
+ self.assertEqual((1,10), ih._get_start_end(size=10))
+ self.assertEqual((0,9), ih._get_start_end(start=0, size=10))
+ self.assertEqual((1,10), ih._get_start_end(end=10, size=10))
+
+ def test_segments(self):
+ # test that address segments are correctly summarized
+ ih = IntelHex()
+ sg = ih.segments()
+ self.assertTrue(isinstance(sg, list))
+ self.assertEqual(len(sg), 0)
+ ih[0x100] = 0
+ sg = ih.segments()
+ self.assertTrue(isinstance(sg, list))
+ self.assertEqual(len(sg), 1)
+ self.assertTrue(isinstance(sg[0], tuple))
+ self.assertTrue(len(sg[0]) == 2)
+ self.assertTrue(sg[0][0] < sg[0][1])
+ self.assertEqual(min(sg[0]), 0x100)
+ self.assertEqual(max(sg[0]), 0x101)
+ ih[0x101] = 1
+ sg = ih.segments()
+ self.assertTrue(isinstance(sg, list))
+ self.assertEqual(len(sg), 1)
+ self.assertTrue(isinstance(sg[0], tuple))
+ self.assertTrue(len(sg[0]) == 2)
+ self.assertTrue(sg[0][0] < sg[0][1])
+ self.assertEqual(min(sg[0]), 0x100)
+ self.assertEqual(max(sg[0]), 0x102)
+ ih[0x200] = 2
+ ih[0x201] = 3
+ ih[0x202] = 4
+ sg = ih.segments()
+ self.assertTrue(isinstance(sg, list))
+ self.assertEqual(len(sg), 2)
+ self.assertTrue(isinstance(sg[0], tuple))
+ self.assertTrue(len(sg[0]) == 2)
+ self.assertTrue(sg[0][0] < sg[0][1])
+ self.assertTrue(isinstance(sg[1], tuple))
+ self.assertTrue(len(sg[1]) == 2)
+ self.assertTrue(sg[1][0] < sg[1][1])
+ self.assertEqual(min(sg[0]), 0x100)
+ self.assertEqual(max(sg[0]), 0x102)
+ self.assertEqual(min(sg[1]), 0x200)
+ self.assertEqual(max(sg[1]), 0x203)
+ ih[0x204] = 5
+ sg = ih.segments()
+ self.assertEqual(len(sg), 3)
+ sg = ih.segments(min_gap=2)
+ self.assertEqual(len(sg), 2)
+ self.assertEqual(min(sg[1]), 0x200)
+ self.assertEqual(max(sg[1]), 0x205)
+ pass
+
+class TestIntelHexLoadBin(TestIntelHexBase):
+
+ def setUp(self):
+ self.bytes = asbytes('0123456789')
+ self.f = BytesIO(self.bytes)
+
+ def tearDown(self):
+ self.f.close()
+
+ def test_loadbin(self):
+ ih = IntelHex()
+ ih.loadbin(self.f)
+ self.assertEqual(0, ih.minaddr())
+ self.assertEqual(9, ih.maxaddr())
+ self.assertEqual(self.bytes, ih.tobinstr())
+
+ def test_bin_fromfile(self):
+ ih = IntelHex()
+ ih.fromfile(self.f, format='bin')
+ self.assertEqual(0, ih.minaddr())
+ self.assertEqual(9, ih.maxaddr())
+ self.assertEqual(self.bytes, ih.tobinstr())
+
+ def test_loadbin_w_offset(self):
+ ih = IntelHex()
+ ih.loadbin(self.f, offset=100)
+ self.assertEqual(100, ih.minaddr())
+ self.assertEqual(109, ih.maxaddr())
+ self.assertEqual(self.bytes, ih.tobinstr())
+
+ def test_loadfile_format_bin(self):
+ ih = IntelHex()
+ ih.loadfile(self.f, format='bin')
+ self.assertEqual(0, ih.minaddr())
+ self.assertEqual(9, ih.maxaddr())
+ self.assertEqual(self.bytes, ih.tobinstr())
+
+
+class TestIntelHexStartingAddressRecords(TestIntelHexBase):
+
+ def _test_read(self, hexstr, data, start_addr):
+ sio = StringIO(hexstr)
+ ih = IntelHex(sio)
+ sio.close()
+ # test data
+ self.assertEqual(data, ih._buf,
+ "Internal buffer: %r != %r" %
+ (data, ih._buf))
+ self.assertEqual(start_addr, ih.start_addr,
+ "Start address: %r != %r" %
+ (start_addr, ih.start_addr))
+
+ def test_read_rectype3(self):
+ self._test_read(hex_rectype3, data_rectype3, start_addr_rectype3)
+
+ def test_read_rectype5(self):
+ self._test_read(hex_rectype5, data_rectype5, start_addr_rectype5)
+
+ def _test_write(self, hexstr, data, start_addr, write_start_addr=True):
+ # prepare
+ ih = IntelHex(None)
+ ih._buf = data
+ ih.start_addr = start_addr
+ # write
+ sio = StringIO()
+ ih.write_hex_file(sio, write_start_addr)
+ s = sio.getvalue()
+ sio.close()
+ # check
+ self.assertEqualWrittenData(hexstr, s)
+
+ def _test_dont_write(self, hexstr, data, start_addr):
+ expected = ''.join(hexstr.splitlines(True)[1:])
+ self._test_write(expected, data, start_addr, False)
+
+ def test_write_rectype3(self):
+ self._test_write(hex_rectype3, data_rectype3, start_addr_rectype3)
+
+ def test_dont_write_rectype3(self):
+ self._test_dont_write(hex_rectype3, data_rectype3, start_addr_rectype3)
+
+ def test_write_rectype5(self):
+ self._test_write(hex_rectype5, data_rectype5, start_addr_rectype5)
+
+ def test_dont_write_rectype5(self):
+ self._test_dont_write(hex_rectype5, data_rectype5, start_addr_rectype5)
+
+ def test_write_invalid_start_addr_value(self):
+ ih = IntelHex()
+ ih.start_addr = {'foo': 1}
+ sio = StringIO()
+ self.assertRaises(InvalidStartAddressValueError, ih.write_hex_file, sio)
+
+
+class TestIntelHex_big_files(TestIntelHexBase):
+ """Test that data bigger than 64K read/write correctly"""
+
+ def setUp(self):
+ self.f = StringIO(hex64k)
+
+ def tearDown(self):
+ self.f.close()
+ del self.f
+
+ def test_readfile(self):
+ ih = intelhex.IntelHex(self.f)
+ for addr, byte in dict_items_g(data64k):
+ readed = ih[addr]
+ self.assertEqual(byte, readed,
+ "data not equal at addr %X "
+ "(%X != %X)" % (addr, byte, readed))
+
+ def test_write_hex_file(self):
+ ih = intelhex.IntelHex(self.f)
+ sio = StringIO()
+ ih.write_hex_file(sio)
+ s = sio.getvalue()
+ sio.close()
+ self.assertEqualWrittenData(hex64k, s)
+
+
+class TestIntelHexGetPutString(TestIntelHexBase):
+
+ def setUp(self):
+ self.ih = IntelHex()
+ for i in range_g(10):
+ self.ih[i] = i
+
+ def test_gets(self):
+ self.assertEqual(asbytes('\x00\x01\x02\x03\x04\x05\x06\x07'), self.ih.gets(0, 8))
+ self.assertEqual(asbytes('\x07\x08\x09'), self.ih.gets(7, 3))
+ self.assertRaisesMsg(intelhex.NotEnoughDataError,
+ 'Bad access at 0x1: '
+ 'not enough data to read 10 contiguous bytes',
+ self.ih.gets, 1, 10)
+
+ def test_puts(self):
+ self.ih.puts(0x03, asbytes('hello'))
+ self.assertEqual(asbytes('\x00\x01\x02hello\x08\x09'), self.ih.gets(0, 10))
+
+ def test_getsz(self):
+ self.assertEqual(asbytes(''), self.ih.getsz(0))
+ self.assertRaisesMsg(intelhex.NotEnoughDataError,
+ 'Bad access at 0x1: '
+ 'not enough data to read zero-terminated string',
+ self.ih.getsz, 1)
+ self.ih[4] = 0
+ self.assertEqual(asbytes('\x01\x02\x03'), self.ih.getsz(1))
+
+ def test_putsz(self):
+ self.ih.putsz(0x03, asbytes('hello'))
+ self.assertEqual(asbytes('\x00\x01\x02hello\x00\x09'), self.ih.gets(0, 10))
+
+ def test_find(self):
+ self.assertEqual(0, self.ih.find(asbytes('\x00\x01\x02\x03\x04\x05\x06')))
+ self.assertEqual(0, self.ih.find(asbytes('\x00')))
+ self.assertEqual(3, self.ih.find(asbytes('\x03\x04\x05\x06')))
+ self.assertEqual(3, self.ih.find(asbytes('\x03')))
+ self.assertEqual(7, self.ih.find(asbytes('\x07\x08\x09')))
+ self.assertEqual(7, self.ih.find(asbytes('\x07')))
+ self.assertEqual(-1, self.ih.find(asbytes('\x0a')))
+ self.assertEqual(-1, self.ih.find(asbytes('\x02\x01')))
+ self.assertEqual(-1, self.ih.find(asbytes('\x08\x07')))
+
+ def test_find_start(self):
+ self.assertEqual(-1, self.ih.find(asbytes('\x00\x01\x02\x03\x04\x05\x06'), start=3))
+ self.assertEqual(-1, self.ih.find(asbytes('\x00'), start=3))
+ self.assertEqual(3, self.ih.find(asbytes('\x03\x04\x05\x06'), start=3))
+ self.assertEqual(3, self.ih.find(asbytes('\x03'), start=3))
+ self.assertEqual(7, self.ih.find(asbytes('\x07\x08\x09'), start=3))
+ self.assertEqual(7, self.ih.find(asbytes('\x07'), start=3))
+ self.assertEqual(-1, self.ih.find(asbytes('\x0a'), start=3))
+ self.assertEqual(-1, self.ih.find(asbytes('\x02\x01'), start=3))
+ self.assertEqual(-1, self.ih.find(asbytes('\x08\x07'), start=3))
+
+ def test_find_end(self):
+ self.assertEqual(-1, self.ih.find(asbytes('\x00\x01\x02\x03\x04\x05\x06'), end=4))
+ self.assertEqual(0, self.ih.find(asbytes('\x00'), end=4))
+ self.assertEqual(-1, self.ih.find(asbytes('\x03\x04\x05\x06'), end=4))
+ self.assertEqual(3, self.ih.find(asbytes('\x03'), end=4))
+ self.assertEqual(-1, self.ih.find(asbytes('\x07\x08\x09'), end=4))
+ self.assertEqual(-1, self.ih.find(asbytes('\x07'), end=4))
+ self.assertEqual(-1, self.ih.find(asbytes('\x0a'), end=4))
+ self.assertEqual(-1, self.ih.find(asbytes('\x02\x01'), end=4))
+ self.assertEqual(-1, self.ih.find(asbytes('\x08\x07'), end=4))
+
+ def test_find_start_end(self):
+ self.assertEqual(-1, self.ih.find(asbytes('\x00\x01\x02\x03\x04\x05\x06'), start=3, end=7))
+ self.assertEqual(-1, self.ih.find(asbytes('\x00'), start=3, end=7))
+ self.assertEqual(3, self.ih.find(asbytes('\x03\x04\x05\x06'), start=3, end=7))
+ self.assertEqual(3, self.ih.find(asbytes('\x03'), start=3, end=7))
+ self.assertEqual(-1, self.ih.find(asbytes('\x07\x08\x09'), start=3, end=7))
+ self.assertEqual(-1, self.ih.find(asbytes('\x07'), start=3, end=7))
+ self.assertEqual(-1, self.ih.find(asbytes('\x0a'), start=3, end=7))
+ self.assertEqual(-1, self.ih.find(asbytes('\x02\x01'), start=3, end=7))
+ self.assertEqual(-1, self.ih.find(asbytes('\x08\x07'), start=3, end=7))
+
+
+class TestIntelHexDump(TestIntelHexBase):
+
+ def test_empty(self):
+ ih = IntelHex()
+ sio = StringIO()
+ ih.dump(sio)
+ self.assertEqual('', sio.getvalue())
+
+ def test_simple(self):
+ ih = IntelHex()
+ ih[0] = 0x12
+ ih[1] = 0x34
+ sio = StringIO()
+ ih.dump(sio)
+ self.assertEqual(
+ '0000 12 34 -- -- -- -- -- -- -- -- -- -- -- -- -- -- |.4 |\n',
+ sio.getvalue())
+ ih[16] = 0x56
+ ih[30] = 0x98
+ sio = StringIO()
+ ih.dump(sio)
+ self.assertEqual(
+ '0000 12 34 -- -- -- -- -- -- -- -- -- -- -- -- -- -- |.4 |\n'
+ '0010 56 -- -- -- -- -- -- -- -- -- -- -- -- -- 98 -- |V . |\n',
+ sio.getvalue())
+
+ def test_minaddr_not_zero(self):
+ ih = IntelHex()
+ ih[16] = 0x56
+ ih[30] = 0x98
+ sio = StringIO()
+ ih.dump(sio)
+ self.assertEqual(
+ '0010 56 -- -- -- -- -- -- -- -- -- -- -- -- -- 98 -- |V . |\n',
+ sio.getvalue())
+
+ def test_start_addr(self):
+ ih = IntelHex()
+ ih[0] = 0x12
+ ih[1] = 0x34
+ ih.start_addr = {'CS': 0x1234, 'IP': 0x5678}
+ sio = StringIO()
+ ih.dump(sio)
+ self.assertEqual(
+ 'CS = 0x1234, IP = 0x5678\n'
+ '0000 12 34 -- -- -- -- -- -- -- -- -- -- -- -- -- -- |.4 |\n',
+ sio.getvalue())
+ ih.start_addr = {'EIP': 0x12345678}
+ sio = StringIO()
+ ih.dump(sio)
+ self.assertEqual(
+ 'EIP = 0x12345678\n'
+ '0000 12 34 -- -- -- -- -- -- -- -- -- -- -- -- -- -- |.4 |\n',
+ sio.getvalue())
+
+ def test_bad_width(self):
+ ih = IntelHex()
+ sio = StringIO()
+ badwidths = [0, -1, -10.5, 2.5]
+ for bw in badwidths:
+ self.assertRaisesMsg(ValueError, "width must be a positive integer.",
+ ih.dump, sio, bw)
+ badwidthtypes = ['', {}, [], sio]
+ for bwt in badwidthtypes:
+ self.assertRaisesMsg(ValueError, "width must be a positive integer.",
+ ih.dump, sio, bwt)
+
+ def test_simple_width3(self):
+ ih = IntelHex()
+ ih[0] = 0x12
+ ih[1] = 0x34
+ sio = StringIO()
+ ih.dump(tofile=sio, width=3)
+ self.assertEqual(
+ '0000 12 34 -- |.4 |\n',
+ sio.getvalue())
+
+ ih[16] = 0x56
+ ih[30] = 0x98
+ sio = StringIO()
+ ih.dump(tofile=sio, width=3)
+ self.assertEqual(
+ '0000 12 34 -- |.4 |\n'
+ '0003 -- -- -- | |\n'
+ '0006 -- -- -- | |\n'
+ '0009 -- -- -- | |\n'
+ '000C -- -- -- | |\n'
+ '000F -- 56 -- | V |\n'
+ '0012 -- -- -- | |\n'
+ '0015 -- -- -- | |\n'
+ '0018 -- -- -- | |\n'
+ '001B -- -- -- | |\n'
+ '001E 98 -- -- |. |\n',
+ sio.getvalue())
+
+ def test_minaddr_not_zero_width3_padding(self):
+ ih = IntelHex()
+ ih[17] = 0x56
+ ih[30] = 0x98
+ sio = StringIO()
+ ih.dump(tofile=sio, width=3, withpadding=True)
+ self.assertEqual(
+ '000F FF FF 56 |..V|\n'
+ '0012 FF FF FF |...|\n'
+ '0015 FF FF FF |...|\n'
+ '0018 FF FF FF |...|\n'
+ '001B FF FF FF |...|\n'
+ '001E 98 FF FF |...|\n',
+ sio.getvalue())
+
+
+class TestIntelHexMerge(TestIntelHexBase):
+
+ def test_merge_empty(self):
+ ih1 = IntelHex()
+ ih2 = IntelHex()
+ ih1.merge(ih2)
+ self.assertEqual({}, ih1.todict())
+
+ def test_merge_simple(self):
+ ih1 = IntelHex({0:1, 1:2, 2:3})
+ ih2 = IntelHex({3:4, 4:5, 5:6})
+ ih1.merge(ih2)
+ self.assertEqual({0:1, 1:2, 2:3, 3:4, 4:5, 5:6}, ih1.todict())
+
+ def test_merge_wrong_args(self):
+ ih1 = IntelHex()
+ self.assertRaisesMsg(TypeError, 'other should be IntelHex object',
+ ih1.merge, {0:1})
+ self.assertRaisesMsg(ValueError, "Can't merge itself",
+ ih1.merge, ih1)
+ ih2 = IntelHex()
+ self.assertRaisesMsg(ValueError, "overlap argument should be either "
+ "'error', 'ignore' or 'replace'",
+ ih1.merge, ih2, overlap='spam')
+
+ def test_merge_overlap(self):
+ # error
+ ih1 = IntelHex({0:1})
+ ih2 = IntelHex({0:2})
+ self.assertRaisesMsg(intelhex.AddressOverlapError,
+ 'Data overlapped at address 0x0',
+ ih1.merge, ih2, overlap='error')
+ # ignore
+ ih1 = IntelHex({0:1})
+ ih2 = IntelHex({0:2})
+ ih1.merge(ih2, overlap='ignore')
+ self.assertEqual({0:1}, ih1.todict())
+ # replace
+ ih1 = IntelHex({0:1})
+ ih2 = IntelHex({0:2})
+ ih1.merge(ih2, overlap='replace')
+ self.assertEqual({0:2}, ih1.todict())
+
+ def test_merge_start_addr(self):
+ # this, None
+ ih1 = IntelHex({'start_addr': {'EIP': 0x12345678}})
+ ih2 = IntelHex()
+ ih1.merge(ih2)
+ self.assertEqual({'start_addr': {'EIP': 0x12345678}}, ih1.todict())
+ # None, other
+ ih1 = IntelHex()
+ ih2 = IntelHex({'start_addr': {'EIP': 0x12345678}})
+ ih1.merge(ih2)
+ self.assertEqual({'start_addr': {'EIP': 0x12345678}}, ih1.todict())
+ # this == other: no conflict
+ ih1 = IntelHex({'start_addr': {'EIP': 0x12345678}})
+ ih2 = IntelHex({'start_addr': {'EIP': 0x12345678}})
+ ih1.merge(ih2)
+ self.assertEqual({'start_addr': {'EIP': 0x12345678}}, ih1.todict())
+ # this != other: conflict
+ ## overlap=error
+ ih1 = IntelHex({'start_addr': {'EIP': 0x12345678}})
+ ih2 = IntelHex({'start_addr': {'EIP': 0x87654321}})
+ self.assertRaisesMsg(AddressOverlapError,
+ 'Starting addresses are different',
+ ih1.merge, ih2, overlap='error')
+ ## overlap=ignore
+ ih1 = IntelHex({'start_addr': {'EIP': 0x12345678}})
+ ih2 = IntelHex({'start_addr': {'EIP': 0x87654321}})
+ ih1.merge(ih2, overlap='ignore')
+ self.assertEqual({'start_addr': {'EIP': 0x12345678}}, ih1.todict())
+ ## overlap=replace
+ ih1 = IntelHex({'start_addr': {'EIP': 0x12345678}})
+ ih2 = IntelHex({'start_addr': {'EIP': 0x87654321}})
+ ih1.merge(ih2, overlap='replace')
+ self.assertEqual({'start_addr': {'EIP': 0x87654321}}, ih1.todict())
+
+
+class TestIntelHex16bit(TestIntelHexBase):
+
+ def setUp(self):
+ self.f = StringIO(hex16)
+
+ def tearDown(self):
+ self.f.close()
+ del self.f
+
+ def test_init_from_file(self):
+ ih = intelhex.IntelHex16bit(self.f)
+
+ def test_init_from_ih(self):
+ ih = intelhex.IntelHex(self.f)
+ ih16 = intelhex.IntelHex16bit(ih)
+
+ def test_default_padding(self):
+ ih16 = intelhex.IntelHex16bit()
+ self.assertEqual(0x0FFFF, ih16.padding)
+ self.assertEqual(0x0FFFF, ih16[0])
+
+ def test_minaddr(self):
+ ih = intelhex.IntelHex16bit(self.f)
+ addr = ih.minaddr()
+ self.assertEqual(0, addr,
+ 'Error in detection of minaddr (0 != 0x%x)' % addr)
+
+ def test_maxaddr(self):
+ ih = intelhex.IntelHex16bit(self.f)
+ addr = ih.maxaddr()
+ self.assertEqual(0x001D, addr,
+ 'Error in detection of maxaddr '
+ '(0x001D != 0x%x)' % addr)
+
+ def test_getitem(self):
+ ih = intelhex.IntelHex16bit(self.f)
+ ih.padding = 0x3FFF
+ for addr, word in enumerate(bin16):
+ self.assertEqual(word, ih[addr],
+ 'Data mismatch at address '
+ '0x%x (0x%x != 0x%x)' % (addr, word, ih[addr]))
+
+ def test_not_enough_data(self):
+ ih = intelhex.IntelHex()
+ ih[0] = 1
+ ih16 = intelhex.IntelHex16bit(ih)
+ self.assertRaisesMsg(BadAccess16bit,
+ 'Bad access at 0x0: '
+ 'not enough data to read 16 bit value',
+ lambda x: ih16[x],
+ 0)
+
+ def test_write_hex_file(self):
+ ih = intelhex.IntelHex16bit(self.f)
+ sio = StringIO()
+ ih.write_hex_file(sio)
+ s = sio.getvalue()
+ sio.close()
+
+ fin = StringIO(s)
+ ih2 = intelhex.IntelHex16bit(fin)
+
+ self.assertEqual(ih.tobinstr(), ih2.tobinstr(),
+ "Written hex file does not equal with original")
+
+ def test_bug_988148(self):
+ # see https://bugs.launchpad.net/intelhex/+bug/988148
+ ih = intelhex.IntelHex16bit(intelhex.IntelHex())
+ ih[0] = 25
+ sio = StringIO()
+ ih.write_hex_file(sio)
+
+ def test_setitem(self):
+ ih = intelhex.IntelHex16bit(self.f)
+
+ old = ih[0]
+ ih[0] = old ^ 0xFFFF
+
+ self.assertNotEqual(old, ih[0],
+ "Setting new value to internal buffer failed")
+
+ def test_tobinarray(self):
+ ih = intelhex.IntelHex16bit()
+ ih[0] = 0x1234
+ ih[1] = 0x5678
+ self.assertEqual(array.array('H', [0x1234,0x5678,0xFFFF]),
+ ih.tobinarray(start=0, end=2))
+ # change padding
+ ih.padding = 0x3FFF
+ self.assertEqual(array.array('H', [0x1234,0x5678,0x3FFF]),
+ ih.tobinarray(start=0, end=2))
+#/class TestIntelHex16bit
+
+
+class TestIntelHexErrors(TestIntelHexBase):
+ """Tests for custom errors classes"""
+
+ def assertEqualExc(self, message, exception):
+ return self.assertEqual(message, str(exception))
+
+ def test_IntelHexError(self):
+ self.assertEqualExc('IntelHex base error', IntelHexError())
+
+ def test_IntelHexError_message(self):
+ self.assertEqualExc('IntelHex custom error message',
+ IntelHexError(msg='IntelHex custom error message'))
+ self.assertEqualExc('IntelHex base error', IntelHexError(msg=''))
+
+ def test_HexReaderError(self):
+ self.assertEqualExc('Hex reader base error', HexReaderError())
+
+ def test_HexRecordError(self):
+ self.assertEqualExc('Hex file contains invalid record at line 1',
+ HexRecordError(line=1))
+
+ def test_RecordLengthError(self):
+ self.assertEqualExc('Record at line 1 has invalid length',
+ RecordLengthError(line=1))
+
+ def test_RecordTypeError(self):
+ self.assertEqualExc('Record at line 1 has invalid record type',
+ RecordTypeError(line=1))
+
+ def test_RecordChecksumError(self):
+ self.assertEqualExc('Record at line 1 has invalid checksum',
+ RecordChecksumError(line=1))
+
+ def test_EOFRecordError(self):
+ self.assertEqualExc('File has invalid End-of-File record',
+ EOFRecordError())
+
+ def test_ExtendedSegmentAddressRecordError(self):
+ self.assertEqualExc(
+ 'Invalid Extended Segment Address Record at line 1',
+ ExtendedSegmentAddressRecordError(line=1))
+
+ def test_ExtendedLinearAddressRecordError(self):
+ self.assertEqualExc('Invalid Extended Linear Address Record at line 1',
+ ExtendedLinearAddressRecordError(line=1))
+
+ def test_StartSegmentAddressRecordError(self):
+ self.assertEqualExc('Invalid Start Segment Address Record at line 1',
+ StartSegmentAddressRecordError(line=1))
+
+ def test_StartLinearAddressRecordError(self):
+ self.assertEqualExc('Invalid Start Linear Address Record at line 1',
+ StartLinearAddressRecordError(line=1))
+
+ def test_DuplicateStartAddressRecord(self):
+ self.assertEqualExc('Start Address Record appears twice at line 1',
+ DuplicateStartAddressRecordError(line=1))
+
+ def test_InvalidStartAddressValue(self):
+ self.assertEqualExc("Invalid start address value: {'foo': 1}",
+ InvalidStartAddressValueError(start_addr={'foo': 1}))
+
+ def test_AddressOverlapError(self):
+ self.assertEqualExc('Hex file has data overlap at address 0x1234 '
+ 'on line 1',
+ AddressOverlapError(address=0x1234, line=1))
+
+ def test_NotEnoughDataError(self):
+ self.assertEqualExc('Bad access at 0x1234: '
+ 'not enough data to read 10 contiguous bytes',
+ intelhex.NotEnoughDataError(address=0x1234, length=10))
+
+ def test_BadAccess16bit(self):
+ self.assertEqualExc('Bad access at 0x1234: '
+ 'not enough data to read 16 bit value',
+ BadAccess16bit(address=0x1234))
+#/class TestIntelHexErrors
+
+
+class TestDecodeHexRecords(TestIntelHexBase):
+ """Testing that decoding of records is correct
+ and all errors raised when needed
+ """
+
+ def setUp(self):
+ self.ih = IntelHex()
+ self.decode_record = self.ih._decode_record
+
+ def tearDown(self):
+ del self.ih
+
+ def test_empty_line(self):
+ # do we could to accept empty lines in hex files?
+ # standard don't say anything about this
+ self.decode_record('')
+
+ def test_non_empty_line(self):
+ self.assertRaisesMsg(HexRecordError,
+ 'Hex file contains invalid record at line 1',
+ self.decode_record,
+ ' ',
+ 1)
+
+ def test_short_record(self):
+ # if record too short it's not a hex record
+ self.assertRaisesMsg(HexRecordError,
+ 'Hex file contains invalid record at line 1',
+ self.decode_record,
+ ':',
+ 1)
+
+ def test_odd_hexascii_digits(self):
+ self.assertRaisesMsg(HexRecordError,
+ 'Hex file contains invalid record at line 1',
+ self.decode_record,
+ ':0100000100F',
+ 1)
+
+ def test_invalid_length(self):
+ self.assertRaisesMsg(RecordLengthError,
+ 'Record at line 1 has invalid length',
+ self.decode_record,
+ ':FF00000100',
+ 1)
+
+ def test_invalid_record_type(self):
+ self.assertRaisesMsg(RecordTypeError,
+ 'Record at line 1 has invalid record type',
+ self.decode_record,
+ ':000000FF01',
+ 1)
+
+ def test_invalid_checksum(self):
+ self.assertRaisesMsg(RecordChecksumError,
+ 'Record at line 1 has invalid checksum',
+ self.decode_record,
+ ':0000000100',
+ 1)
+
+ def test_invalid_eof(self):
+ self.assertRaisesMsg(EOFRecordError,
+ 'File has invalid End-of-File record',
+ self.decode_record,
+ ':0100000100FE',
+ 1)
+
+ def test_invalid_extended_segment(self):
+ # length
+ self.assertRaisesMsg(ExtendedSegmentAddressRecordError,
+ 'Invalid Extended Segment Address Record at line 1',
+ self.decode_record,
+ ':00000002FE',
+ 1)
+ # addr field
+ self.assertRaisesMsg(ExtendedSegmentAddressRecordError,
+ 'Invalid Extended Segment Address Record at line 1',
+ self.decode_record,
+ ':020001020000FB',
+ 1)
+
+ def test_invalid_linear_address(self):
+ # length
+ self.assertRaisesMsg(ExtendedLinearAddressRecordError,
+ 'Invalid Extended Linear Address Record '
+ 'at line 1',
+ self.decode_record,
+ ':00000004FC',
+ 1)
+ # addr field
+ self.assertRaisesMsg(ExtendedLinearAddressRecordError,
+ 'Invalid Extended Linear Address Record '
+ 'at line 1',
+ self.decode_record,
+ ':020001040000F9',
+ 1)
+
+ def test_invalid_start_segment_addr(self):
+ # length
+ self.assertRaisesMsg(StartSegmentAddressRecordError,
+ 'Invalid Start Segment Address Record at line 1',
+ self.decode_record,
+ ':00000003FD',
+ 1)
+ # addr field
+ self.assertRaisesMsg(StartSegmentAddressRecordError,
+ 'Invalid Start Segment Address Record at line 1',
+ self.decode_record,
+ ':0400010300000000F8',
+ 1)
+
+ def test_duplicate_start_segment_addr(self):
+ self.decode_record(':0400000312345678E5')
+ self.assertRaisesMsg(DuplicateStartAddressRecordError,
+ 'Start Address Record appears twice at line 2',
+ self.decode_record,
+ ':0400000300000000F9',
+ 2)
+
+ def test_invalid_start_linear_addr(self):
+ # length
+ self.assertRaisesMsg(StartLinearAddressRecordError,
+ 'Invalid Start Linear Address Record at line 1',
+ self.decode_record,
+ ':00000005FB',
+ 1)
+ # addr field
+ self.assertRaisesMsg(StartLinearAddressRecordError,
+ 'Invalid Start Linear Address Record at line 1',
+ self.decode_record,
+ ':0400010500000000F6',
+ 1)
+
+ def test_duplicate_start_linear_addr(self):
+ self.decode_record(':0400000512345678E3')
+ self.assertRaisesMsg(DuplicateStartAddressRecordError,
+ 'Start Address Record appears twice at line 2',
+ self.decode_record,
+ ':0400000500000000F7',
+ 2)
+
+ def test_addr_overlap(self):
+ self.decode_record(':0100000000FF')
+ self.assertRaisesMsg(AddressOverlapError,
+ 'Hex file has data overlap at address 0x0 '
+ 'on line 1',
+ self.decode_record,
+ ':0100000000FF',
+ 1)
+
+ def test_data_record(self):
+ # should be no exceptions
+ self.decode_record(':0100000000FF\n')
+ self.decode_record(':03000100000102F9\r\n')
+ self.decode_record(':1004E300CFF0FBE2FDF220FF20F2E120E2FBE6F396')
+
+ def test_eof(self):
+ # EOF should raise special exception
+ self.assertRaises(_EndOfFile, self.decode_record, ':00000001FF')
+
+#/class TestDecodeHexRecords
+
+
+class TestHex2Bin(unittest.TestCase):
+
+ def setUp(self):
+ self.fin = StringIO(hex8)
+ self.fout = BytesIO()
+
+ def tearDown(self):
+ self.fin.close()
+ self.fout.close()
+
+ def test_hex2bin(self):
+ ih = hex2bin(self.fin, self.fout)
+ data = array.array('B', asbytes(self.fout.getvalue()))
+ for addr in range_g(len(bin8)):
+ expected = bin8[addr]
+ actual = data[addr]
+ self.assertEqual(expected, actual,
+ "Data different at address "
+ "%x (%x != %x)" % (addr, expected, actual))
+
+
+class TestDiffDumps(unittest.TestCase):
+
+ def test_simple(self):
+ ih1 = IntelHex({1:0x30, 20:0x31, 40:0x33})
+ ih2 = IntelHex({1:0x30, 20:0x32, 40:0x33})
+ sio = StringIO()
+ intelhex.diff_dumps(ih1, ih2, sio)
+ result = sio.getvalue()
+ extra = ' '
+ if sys.version_info[0] >= 3 or sys.version >= '2.7':
+ extra = ''
+ shouldbe = (
+ "--- a%(extra)s\n"
+ "+++ b%(extra)s\n"
+ "@@ -1,3 +1,3 @@\n"
+ " 0000 -- 30 -- -- -- -- -- -- -- -- -- -- -- -- -- -- | 0 |\n"
+ "-0010 -- -- -- -- 31 -- -- -- -- -- -- -- -- -- -- -- | 1 |\n"
+ "+0010 -- -- -- -- 32 -- -- -- -- -- -- -- -- -- -- -- | 2 |\n"
+ " 0020 -- -- -- -- -- -- -- -- 33 -- -- -- -- -- -- -- | 3 |\n"
+ ) % dict(extra=extra)
+ self.assertEqual(shouldbe, result)
+
+
+class TestBuildRecords(TestIntelHexBase):
+
+ def test__from_bytes(self):
+ self.assertEqual(':00000001FF',
+ intelhex.Record._from_bytes([0,0,0,1]))
+
+ def test_data(self):
+ self.assertEqual(':011234005663', intelhex.Record.data(0x1234, [0x56]))
+ self.assertEqual(':0312340056789059',
+ intelhex.Record.data(0x1234, [0x56, 0x78, 0x90]))
+
+ def test_eof(self):
+ self.assertEqual(':00000001FF', intelhex.Record.eof())
+
+ def test_extended_segment_address(self):
+ self.assertEqual(':020000021234B6',
+ intelhex.Record.extended_segment_address(0x1234))
+
+ def test_start_segment_address(self):
+ self.assertEqual(':0400000312345678E5',
+ intelhex.Record.start_segment_address(0x1234, 0x5678))
+
+ def test_extended_linear_address(self):
+ self.assertEqual(':020000041234B4',
+ intelhex.Record.extended_linear_address(0x1234))
+
+ def test_start_linear_address(self):
+ self.assertEqual(':0400000512345678E3',
+ intelhex.Record.start_linear_address(0x12345678))
+
+
+class Test_GetFileAndAddrRange(TestIntelHexBase):
+
+ def test_simple(self):
+ self.assertEqual(('filename.hex', None, None),
+ intelhex._get_file_and_addr_range('filename.hex'))
+ self.assertEqual(('f', None, None),
+ intelhex._get_file_and_addr_range('f'))
+ self.assertEqual(('filename.hex', 1, None),
+ intelhex._get_file_and_addr_range('filename.hex:1:'))
+ self.assertEqual(('filename.hex', None, 10),
+ intelhex._get_file_and_addr_range('filename.hex::A'))
+ self.assertEqual(('filename.hex', 1, 10),
+ intelhex._get_file_and_addr_range('filename.hex:1:A'))
+ self.assertEqual(('filename.hex', 1, 10),
+ intelhex._get_file_and_addr_range('filename.hex:0001:000A'))
+
+ def test_bad_notation(self):
+ self.assertRaises(intelhex._BadFileNotation,
+ intelhex._get_file_and_addr_range, 'filename.hex:')
+ self.assertRaises(intelhex._BadFileNotation,
+ intelhex._get_file_and_addr_range, 'filename.hex:::')
+ self.assertRaises(intelhex._BadFileNotation,
+ intelhex._get_file_and_addr_range, 'C:\\filename.hex:', True)
+
+ def test_drive_letter(self):
+ self.assertEqual(('C:\\filename.hex', None, None),
+ intelhex._get_file_and_addr_range('C:\\filename.hex', True))
+ self.assertEqual(('C:\\filename.hex', 1, None),
+ intelhex._get_file_and_addr_range('C:\\filename.hex:1:', True))
+ self.assertEqual(('C:\\filename.hex', None, 10),
+ intelhex._get_file_and_addr_range('C:\\filename.hex::A', True))
+ self.assertEqual(('C:\\filename.hex', 1, 10),
+ intelhex._get_file_and_addr_range('C:\\filename.hex:1:A', True))
+ self.assertEqual(('C:\\filename.hex', 1, 10),
+ intelhex._get_file_and_addr_range('C:\\filename.hex:0001:000A', True))
+
+
+class TestXrangeLongInt(unittest.TestCase):
+
+ def test_xrange_longint(self):
+ # Bug #1408934: xrange(longint) blows with OverflowError:
+ if compat.Python == 2:
+ self.assertRaises(OverflowError, xrange, sys.maxint, sys.maxint+3)
+ #
+ upr = compat.range_g(2684625744, 2684625747)
+ self.assertEqual([2684625744, 2684625745, 2684625746], list(upr))
+ upr = compat.range_g(2684625744, 2684625747, 2)
+ self.assertEqual([2684625744, 2684625746], list(upr))
+ #
+ dnr = compat.range_g(2684625746, 2684625743, -1)
+ self.assertEqual([2684625746, 2684625745, 2684625744], list(dnr))
+ dnr = compat.range_g(2684625746, 2684625743, -2)
+ self.assertEqual([2684625746, 2684625744], list(dnr))
+
+
+class TestInSubprocess(unittest.TestCase):
+
+ def runProcessAndGetAsciiStdoutOrStderr(self, cmdline):
+ if sys.platform != 'win32':
+ cmdline = shlex.split(cmdline)
+ p = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ retcode = p.poll()
+ if stdout:
+ output = stdout.decode('ascii', 'replace')
+ elif stderr:
+ output = stderr.decode('ascii', 'replace')
+ output = output.replace('\r', '')
+ return retcode, output
+
+ def versionChecker(self, cmdline_template):
+ cmdline = cmdline_template % sys.executable
+ retcode, output = self.runProcessAndGetAsciiStdoutOrStderr(cmdline)
+ self.assertEqual(version_str, output.rstrip())
+ self.assertEqual(0, retcode)
+
+ def test_setup_version(self):
+ self.versionChecker('%s setup.py --version')
+
+ def test_sripts_bin2hex_version(self):
+ self.versionChecker('%s scripts/bin2hex.py --version')
+
+ def test_sripts_hex2bin_version(self):
+ self.versionChecker('%s scripts/hex2bin.py --version')
+
+ def test_sripts_hex2dump_version(self):
+ self.versionChecker('%s scripts/hex2dump.py --version')
+
+ def test_sripts_hexdiff_version(self):
+ self.versionChecker('%s scripts/hexdiff.py --version')
+
+ def test_sripts_hexmerge_version(self):
+ self.versionChecker('%s scripts/hexmerge.py --version')
+
+
+class TestWriteHexFileByteCount(unittest.TestCase):
+
+ def setUp(self):
+ self.f = StringIO(hex8)
+
+ def tearDown(self):
+ self.f.close()
+ del self.f
+
+ def test_write_hex_file_bad_byte_count(self):
+ ih = intelhex.IntelHex(self.f)
+ sio = StringIO()
+ self.assertRaises(ValueError, ih.write_hex_file, sio, byte_count=0)
+ self.assertRaises(ValueError, ih.write_hex_file, sio, byte_count=-1)
+ self.assertRaises(ValueError, ih.write_hex_file, sio, byte_count=256)
+
+ def test_write_hex_file_byte_count_1(self):
+ ih = intelhex.IntelHex(self.f)
+ ih1 = ih[:4]
+ sio = StringIO()
+ ih1.write_hex_file(sio, byte_count=1)
+ s = sio.getvalue()
+ sio.close()
+ # check that we have all data records with data length == 1
+ self.assertEqual((
+ ':0100000002FD\n'
+ ':0100010005F9\n'
+ ':01000200A25B\n'
+ ':01000300E517\n'
+ ':00000001FF\n'
+ ), s,
+ "Written hex is not in byte count 1")
+ # read back and check content
+ fin = StringIO(s)
+ ih2 = intelhex.IntelHex(fin)
+ self.assertEqual(ih1.tobinstr(), ih2.tobinstr(),
+ "Written hex file does not equal with original")
+
+ def test_write_hex_file_byte_count_13(self):
+ ih = intelhex.IntelHex(self.f)
+ sio = StringIO()
+ ih.write_hex_file(sio, byte_count=13)
+ s = sio.getvalue()
+ # control written hex first line to check that byte count is 13
+ sio.seek(0)
+ self.assertEqual(sio.readline(),
+ ':0D0000000205A2E576246AF8E6057622786E\n',
+ "Written hex is not in byte count 13")
+ sio.close()
+
+ fin = StringIO(s)
+ ih2 = intelhex.IntelHex(fin)
+
+ self.assertEqual(ih.tobinstr(), ih2.tobinstr(),
+ "Written hex file does not equal with original")
+
+ def test_write_hex_file_byte_count_255(self):
+ ih = intelhex.IntelHex(self.f)
+ sio = StringIO()
+ ih.write_hex_file(sio, byte_count=255)
+ s = sio.getvalue()
+ # control written hex first line to check that byte count is 255
+ sio.seek(0)
+ self.assertEqual(sio.readline(),
+ (':FF0000000205A2E576246AF8E60576227867300702786AE475F0011204AD02'
+ '04552000EB7F2ED2008018EF540F2490D43440D4FF30040BEF24BFB41A0050'
+ '032461FFE57760021577057AE57A7002057930070D7867E475F0011204ADEF'
+ '02049B02057B7403D2078003E4C207F5768B678A688969E4F577F579F57AE5'
+ '7760077F2012003E80F57578FFC201C200C202C203C205C206C20812000CFF'
+ '700D3007057F0012004FAF7AAE7922B4255FC2D5C20412000CFF24D0B40A00'
+ '501A75F00A787730D50508B6FF0106C6A426F620D5047002D20380D924CFB4'
+ '1A00EF5004C2E5D20402024FD20180C6D20080C0D20280BCD2D580BAD20580'
+ 'B47F2012003E20020774010E\n'),
+ "Written hex is not in byte count 255")
+ sio.close()
+
+ fin = StringIO(s)
+ ih2 = intelhex.IntelHex(fin)
+
+ self.assertEqual(ih.tobinstr(), ih2.tobinstr(),
+ "Written hex file does not equal with original")
+
+##
+# MAIN
+if __name__ == '__main__':
+ unittest.main()
diff --git a/mixly/tools/python/kflash.py b/mixly/tools/python/kflash.py
new file mode 100644
index 00000000..960e5648
--- /dev/null
+++ b/mixly/tools/python/kflash.py
@@ -0,0 +1,1457 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from __future__ import (division, print_function)
+
+import sys
+import time
+import zlib
+import copy
+import struct
+import binascii
+import hashlib
+import argparse
+import math
+import zipfile, tempfile, backports.tempfile
+import json
+import re
+import os
+
+
+class KFlash:
+ print_callback = None
+
+ def __init__(self, print_callback = None):
+ self.killProcess = False
+ self.loader = None
+ self.print_callback = print_callback
+
+ @staticmethod
+ def log(*args, **kwargs):
+ if KFlash.print_callback:
+ # pylint: disable=not-callable
+ KFlash.print_callback(*args, **kwargs)
+ # pylint: enable=not-callable
+ else:
+ print(*args, **kwargs)
+
+ def process(self, terminal=True, dev="", baudrate=1500000, board=None, sram = False, file="", callback=None, noansi=False, terminal_auto_size=False, terminal_size=(50, 1), slow_mode = False):
+ self.killProcess = False
+ BASH_TIPS = dict(NORMAL='\033[0m',BOLD='\033[1m',DIM='\033[2m',UNDERLINE='\033[4m',
+ DEFAULT='\033[0m', RED='\033[31m', YELLOW='\033[33m', GREEN='\033[32m',
+ BG_DEFAULT='\033[49m', BG_WHITE='\033[107m')
+
+ ERROR_MSG = BASH_TIPS['RED']+BASH_TIPS['BOLD']+'[ERROR]'+BASH_TIPS['NORMAL']
+ WARN_MSG = BASH_TIPS['YELLOW']+BASH_TIPS['BOLD']+'[WARN]'+BASH_TIPS['NORMAL']
+ INFO_MSG = BASH_TIPS['GREEN']+BASH_TIPS['BOLD']+'[INFO]'+BASH_TIPS['NORMAL']
+
+ VID_LIST_FOR_AUTO_LOOKUP = "(1A86)|(0403)|(067B)|(10C4)|(C251)|(0403)"
+ # WCH FTDI PL CL DAP OPENEC
+ ISP_RECEIVE_TIMEOUT = 1
+
+ MAX_RETRY_TIMES = 10
+
+ ISP_FLASH_SECTOR_SIZE = 4096
+ ISP_FLASH_DATA_FRAME_SIZE = ISP_FLASH_SECTOR_SIZE * 16
+
+ def tuple2str(t):
+ ret = ""
+ for i in t:
+ ret += i+" "
+ return ret
+
+ def raise_exception(exception):
+ if self.loader:
+ try:
+ self.loader._port.close()
+ except Exception:
+ pass
+ raise exception
+
+ try:
+ from enum import Enum
+ except ImportError:
+ err = (ERROR_MSG,'enum34 must be installed, run '+BASH_TIPS['GREEN']+'`' + ('pip', 'pip3')[sys.version_info > (3, 0)] + ' install enum34`',BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ raise Exception(err)
+ try:
+ import serial
+ import serial.tools.list_ports
+ except ImportError:
+ err = (ERROR_MSG,'PySerial must be installed, run '+BASH_TIPS['GREEN']+'`' + ('pip', 'pip3')[sys.version_info > (3, 0)] + ' install pyserial`',BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ raise Exception(err)
+
+ class TimeoutError(Exception): pass
+
+ class ProgramFileFormat(Enum):
+ FMT_BINARY = 0
+ FMT_ELF = 1
+ FMT_KFPKG = 2
+
+ # AES is from: https://github.com/ricmoo/pyaes, Copyright by Richard Moore
+ class AES:
+ '''Encapsulates the AES block cipher.
+ You generally should not need this. Use the AESModeOfOperation classes
+ below instead.'''
+ @staticmethod
+ def _compact_word(word):
+ return (word[0] << 24) | (word[1] << 16) | (word[2] << 8) | word[3]
+
+ # Number of rounds by keysize
+ number_of_rounds = {16: 10, 24: 12, 32: 14}
+
+ # Round constant words
+ rcon = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ]
+
+ # S-box and Inverse S-box (S is for Substitution)
+ S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ]
+ Si =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ]
+
+ # Transformations for encryption
+ T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ]
+ T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ]
+ T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ]
+ T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ]
+
+ # Transformations for decryption
+ T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ]
+ T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ]
+ T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ]
+ T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ]
+
+ # Transformations for decryption key expansion
+ U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ]
+ U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ]
+ U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ]
+ U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ]
+
+ def __init__(self, key):
+
+ if len(key) not in (16, 24, 32):
+ raise_exception( ValueError('Invalid key size') )
+
+ rounds = self.number_of_rounds[len(key)]
+
+ # Encryption round keys
+ self._Ke = [[0] * 4 for i in range(rounds + 1)]
+
+ # Decryption round keys
+ self._Kd = [[0] * 4 for i in range(rounds + 1)]
+
+ round_key_count = (rounds + 1) * 4
+ KC = len(key) // 4
+
+ # Convert the key into ints
+ tk = [ struct.unpack('>i', key[i:i + 4])[0] for i in range(0, len(key), 4) ]
+
+ # Copy values into round key arrays
+ for i in range(0, KC):
+ self._Ke[i // 4][i % 4] = tk[i]
+ self._Kd[rounds - (i // 4)][i % 4] = tk[i]
+
+ # Key expansion (fips-197 section 5.2)
+ rconpointer = 0
+ t = KC
+ while t < round_key_count:
+
+ tt = tk[KC - 1]
+ tk[0] ^= ((self.S[(tt >> 16) & 0xFF] << 24) ^
+ (self.S[(tt >> 8) & 0xFF] << 16) ^
+ (self.S[ tt & 0xFF] << 8) ^
+ self.S[(tt >> 24) & 0xFF] ^
+ (self.rcon[rconpointer] << 24))
+ rconpointer += 1
+
+ if KC != 8:
+ for i in range(1, KC):
+ tk[i] ^= tk[i - 1]
+
+ # Key expansion for 256-bit keys is "slightly different" (fips-197)
+ else:
+ for i in range(1, KC // 2):
+ tk[i] ^= tk[i - 1]
+ tt = tk[KC // 2 - 1]
+
+ tk[KC // 2] ^= (self.S[ tt & 0xFF] ^
+ (self.S[(tt >> 8) & 0xFF] << 8) ^
+ (self.S[(tt >> 16) & 0xFF] << 16) ^
+ (self.S[(tt >> 24) & 0xFF] << 24))
+
+ for i in range(KC // 2 + 1, KC):
+ tk[i] ^= tk[i - 1]
+
+ # Copy values into round key arrays
+ j = 0
+ while j < KC and t < round_key_count:
+ self._Ke[t // 4][t % 4] = tk[j]
+ self._Kd[rounds - (t // 4)][t % 4] = tk[j]
+ j += 1
+ t += 1
+
+ # Inverse-Cipher-ify the decryption round key (fips-197 section 5.3)
+ for r in range(1, rounds):
+ for j in range(0, 4):
+ tt = self._Kd[r][j]
+ self._Kd[r][j] = (self.U1[(tt >> 24) & 0xFF] ^
+ self.U2[(tt >> 16) & 0xFF] ^
+ self.U3[(tt >> 8) & 0xFF] ^
+ self.U4[ tt & 0xFF])
+
+ def encrypt(self, plaintext):
+ 'Encrypt a block of plain text using the AES block cipher.'
+
+ if len(plaintext) != 16:
+ raise_exception( ValueError('wrong block length') )
+
+ rounds = len(self._Ke) - 1
+ (s1, s2, s3) = [1, 2, 3]
+ a = [0, 0, 0, 0]
+
+ # Convert plaintext to (ints ^ key)
+ t = [(AES._compact_word(plaintext[4 * i:4 * i + 4]) ^ self._Ke[0][i]) for i in range(0, 4)]
+
+ # Apply round transforms
+ for r in range(1, rounds):
+ for i in range(0, 4):
+ a[i] = (self.T1[(t[ i ] >> 24) & 0xFF] ^
+ self.T2[(t[(i + s1) % 4] >> 16) & 0xFF] ^
+ self.T3[(t[(i + s2) % 4] >> 8) & 0xFF] ^
+ self.T4[ t[(i + s3) % 4] & 0xFF] ^
+ self._Ke[r][i])
+ t = copy.copy(a)
+
+ # The last round is special
+ result = [ ]
+ for i in range(0, 4):
+ tt = self._Ke[rounds][i]
+ result.append((self.S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
+ result.append((self.S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
+ result.append((self.S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
+ result.append((self.S[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF)
+
+ return result
+
+ def decrypt(self, ciphertext):
+ 'Decrypt a block of cipher text using the AES block cipher.'
+
+ if len(ciphertext) != 16:
+ raise_exception( ValueError('wrong block length') )
+
+ rounds = len(self._Kd) - 1
+ (s1, s2, s3) = [3, 2, 1]
+ a = [0, 0, 0, 0]
+
+ # Convert ciphertext to (ints ^ key)
+ t = [(AES._compact_word(ciphertext[4 * i:4 * i + 4]) ^ self._Kd[0][i]) for i in range(0, 4)]
+
+ # Apply round transforms
+ for r in range(1, rounds):
+ for i in range(0, 4):
+ a[i] = (self.T5[(t[ i ] >> 24) & 0xFF] ^
+ self.T6[(t[(i + s1) % 4] >> 16) & 0xFF] ^
+ self.T7[(t[(i + s2) % 4] >> 8) & 0xFF] ^
+ self.T8[ t[(i + s3) % 4] & 0xFF] ^
+ self._Kd[r][i])
+ t = copy.copy(a)
+
+ # The last round is special
+ result = [ ]
+ for i in range(0, 4):
+ tt = self._Kd[rounds][i]
+ result.append((self.Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
+ result.append((self.Si[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
+ result.append((self.Si[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
+ result.append((self.Si[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF)
+
+ return result
+
+ class AES_128_CBC:
+
+ def __init__(self, key, iv = None):
+ self._aes = AES(key)
+ if iv is None:
+ self._last_cipherblock = [ 0 ] * 16
+ elif len(iv) != 16:
+ raise_exception( ValueError('initialization vector must be 16 bytes') )
+ else:
+ self._last_cipherblock = iv
+
+
+ def encrypt(self, plaintext):
+ if len(plaintext) != 16:
+ raise_exception( ValueError('plaintext block must be 16 bytes') )
+
+ precipherblock = [ (p ^ l) for (p, l) in zip(plaintext, self._last_cipherblock) ]
+ self._last_cipherblock = self._aes.encrypt(precipherblock)
+
+ return b''.join(map(lambda x: x.to_bytes(1, 'little'), self._last_cipherblock))
+
+ def decrypt(self, ciphertext):
+ if len(ciphertext) != 16:
+ raise_exception( ValueError('ciphertext block must be 16 bytes') )
+
+ cipherblock = ciphertext
+ plaintext = [ (p ^ l) for (p, l) in zip(self._aes.decrypt(cipherblock), self._last_cipherblock) ]
+ self._last_cipherblock = cipherblock
+
+ return b''.join(map(lambda x: x.to_bytes(1, 'little'), plaintext))
+
+ ISP_PROG = '789cbcbc7d5c1357d6007c6792c924080a0e1890d82201a2acebc38a4aab960d428862eb636b85ea56171d10b5dacafa416dcb4a48263122453a60c0604bb182b25bd71535ada8011569edf787623fb4688080a2420522cac77befcc04d0769f7dff787faffec2ccbdf7dc73cf3df79c73cfb91f33f1e0dd331f5ec300061efdb7695144c4a64541f0a746bfe9161c00363b7bcca6dd2f44e8d4588c2e069bab9b8bc5ea62b1385d1ca6d169b0785d3ca6d569b179ba79d87cdd7c2c4197802dd02dc09ed53d8b3da77b0e5ba85b88fdafee7f8b576d2ac52336794780c500747c077f8b31f884bfc5387cc2df62117cc2df62317cc2df62023ee16fb1043ee16f31099ff0b7580a9ff0b758069ff0b7d8033ee16ff128f884bfc59ef0097f8bbde013fe168f864ff85b3c063ee1cfe283015617532cff9b0c50f85117f537bc6f53a8ba93c2fc40343636ffb5a08db12b7dc635de0b0e9a4eff8ce346bc6d2c5d84e32b7dc6375a7058371bbfbd49a9ee1c2ddafb0e4e4fc4f4477140efc041f6aa89d86b20287013088a40ef58baa4c9a37574fbd88e715de3ef3fd157d658de54d97ab8fd68c789ae93f7cff4cdbd1edf9cd0b6f0f60b9d89ddcb7a57f4dfb8ded27cb3edceed5f3b7bba1ff40ef4ab82306f55888fb76ad2446fd5943f79ab8262c6aa429e1fab9ab472ac6a4afa585590ce571592efab9af4beaf6acabf7d5541b5e35421df8c534dba3e4e35a5731cac1f00eb07c0fa01b07e00ac1f08eb07c2fa81b07e20ac3f01d69f00eb4f80f527c0fa4fc2fa4fc2fa4fc2fa4f6e0a0d9a7e2fd47bba87d117a79710e06ed0918501a57723364edae4ed37bd2c79eeca1b2bf1bf95fd6deea61b9b2429e529f1a92da992ade55be3335a323cd654ae49587b73adc71b956f24bc79f3cdd1eb0faf5fb8e1ce86d17f3ffcf785dbef6c2761df4c413a6f7222e6639aa8f32183b1b1a660dd5852895126a58e2243305f5388ce970cc5fc4ca13a3f320c1b670ad38d235598dca4d2c9c94998bf6992ce9f9c8c059826eb02c8706cbc295c379efc031668fa832e909c82294c53740af28fd804d31f7513c8a9d813a6a9ba27c8ffc19e34fd8fee490ac382d038010047a9d1a3696cebb8f6f11d4f7405df0febab6c3cdc74b4f544fbc98e335d67ef5fe84bb8beb0f985b6c4dbcb3a5774afea5ddd7ff3fa9de65fdb7a6e3fe81ce8be171c01f6f4de0bf69ebea71f2f93947b548e3e3cf6e8b813e34f3e71a6acacbcbcb2f2f0e1a3474f9c3879f2cc99b9fbe32b12feb1f05f2f5425da9655afb0dfd8df5271f31f77fef56b558fed41f5809d97adb99fbb650bc995f24830b02c02c099551a22945f74978bfe8d037ce5977fdbb47b1ab803ff8e9d6e0b06c09a8def409a969d1cbc72eeaa1babf04d659be66ebeb159925a9e1abfba65b524a33c23fef596d73dd656ae4d5877739dc79b956f26bc75f3add11b0e6f58f8ea9d57476f3fbc7d6cd0d1a01726fe3a71ecb4a3d35e88fc35725cc88990c4d09ed071334fcc4c8cea891a3fe9e4a465931f4c1e3febe4ac65b31fcc7e62ca99292bfe38f0c727a2cf44aff8f3c09fdddc8df191346d52a93b192dad7001426351a39e349aee65c4456c028bbcbbed166f94937ca42c19d3c460d95972020314c1b45848d85713d9429b2b805c04b22851ec715a2402b8de0da369b2c027cbd439ccb9b98c595b55d78deb3ee561f547ddb0088fbede749db98b68b100d45ad0030b09b25893ea88f933a1bd5fb812f82b4b66b472584789bbae217a1006049bf661eeb9181fbec5d026be56e8cf0246ae5e0c06db066940df2397624034efa51f27686743547c4fd27eb2c05cfd276b7ef29a5742cff6f7c722d3480c53159d0b34f50c5e3947688bea2b0be909c771f8f6d9e78b75a1812602d05d2740a04902749a4006a6e8669cf523715d7ef1bb53f7740e280f9240f90109304d76164188b4dd831f7d9769171d242591c409c0104a4060d76d3c35af7fc353c3cc13e8f996cf5ff32da4a6483a448df2033906e918775cbc6b314502bc9ab0a9a79a1ea363fd301d8476ea9ea2cf2802e0cccdfcf8a224bf1cc7130dbf4bd9161ba24aa421206507bf4bb5ef5a8c30ce961e57d39baf8040528ab0ef80d8d7b4021efbd43dc5ef6eb0454a61ad7952d49f5292ecb5633ec5da4900ce39f18c46d2ae1477027e541b3b6f40192256f263ce9ab4e74be840d2cd5bdf9863dfbd087f459f7dbf5817865aa6375f86adc23ec5073230f56a3360c7c156df0978f74461fa233de038ca5c028c0471f453fbb6f4b0f6fea44ac3164d7087a44be9db09e44d0050e20f8ef2f29d75a5b969825bd6bf7f46780b6ad069428577f5d57f6b266a2c119c5ea7d5a17ceebd7175be66b2f01eb4ba50f347e13d39b558f327377cca3ecd0c373cad48e6a475b6eb582d27b7fb25aea3eff3124cb88e5de2dfa4aea3823e5595d032c8bd2b9f29cb49dcdd4728e148223c39d859aec3ba783ea7622e166fd670b991aec33c97b37e2adb886fbe9182693490d39385fe645d982fbcd57ca24394f27dff52b19eab3dc75589dadfbf833c7888a7887455fea02db4613ec1ad85da704304f060187e3c817b3cc1e77aad0ea03155822e3037058d2ce643119d40d22ed0d227d2744208c28ccab6a59bb54acf4e604d22b0fdf0c926318075b9c60413cb73255dc11d539ba6352a27bcefc6fe73596af6ea49d2a9c67c8d203f5f107142d9032cd4318a19403de5fb89f9b064563f92350ebbf831eced4a3f37deaccbbc6cbab1667de2964a840348464a6ccd776e3916d2756ed8e5e9658d0bbb5ee8486c5fd6baa2c9d814d69770bfd23c75e73403eb01bca95100b78c82e3999353206b2f4f96a4dcd83c775bd936fc8d1bebe6be52f60aa6590971b0aede31b34d0fd4274df4a6265066b21a4830a124cc4c6fb6024a068da36d14f89659c8044a6420dbc2ca65b88ead3684c454cb9ec6a2f75eef5b6e9b90fec27d6aaf044fec5ad6b1aa955a7613937f0ee5dcf39fff237b257f5d59ebc5c6857d2bda8dede54de186a9e6a3e61339abf6068e0a0cb6eedc39917e4f065a92e3a15720597b63dbdc37cadec0dfbcf1cadcf5982614d1062d8bd52083f62d0a63c5002fd4d27732447a9507b861c936e56a794e8b054e1b00253770dcfe214fe9d709ca8d0970d426b9397e88b58821eda125d1c5d5e662f552db87ffa57e7f9252c26329d46cb0b3e41151f479413f740534924f91d69965d6565b9c40de0380287e4f26b29ef5e0873c3e2df93b21a4832fe854256795958963c28c1719374dea8f8edb20de51d1e7adb0e5e80b1617d227d59b103b7c1369c93747623f91f128f6b0d7ddd8116e96f40e5656f68cd6208c124829872b6deb6ea8c9e85d5fafda6ae986cfba199bf6c557c306aaf79c0097f22c1097fec2ea4db3e50d029d8ab3caf0c431efa31e8f7e943253fa30366d3a8f6dc9abbfc5667c95c7c6e35a3de64f36269e9ff5f65f7d3a3da1234a2605d083ea8a1487629506229e85da2ad32c6c97f4ed47f3f0d30620580909f408bcbbc5487f59e253acbc90e75bf2ff5a4938037c46e28a67034d72902dd7b1fe9f7f6951665500ff676bd36a49badb092047462bbebeb171eee6b2cdf8b61b6b31cd44a4ff490e8b73c037c5468a413e4999e240b985dedb027c5b154d3638bb9c2950fe530cda6c8a56ffa63a3bd2466f2f497b702bb2dfe106c106cc310fc9558d3a9811de52ad0401dc29f59f052f62d68d64de2a553230fe899f182fd3c863112f684c5fef0744f50428237fc8b39a9cbfb27b4851ed3b7e5a7a6dc313706001453afb584b1838692989dd961edc11d69ed02ae95ad874d272d430d5106ece3689ea0ae1583080d0220ba3af3f00ce92e8791cdc203f85fca737bb406ed29252914a0664398483ea768d11f4610f169a9f34bf590f4b58a70a28837a812ee9c3838a18734e6f030f536a13f8fd0f2c34b3f70b1b2afbe2277dfd1238c61a48791da4fc529efe422aa08c0c382b41ef5bc00dc9d5b7f5f597802e0cf54b54df0ce672cf6e60633ebacd32c4b455e4f2bc89aafea480bb6cbd6b907ebb073727edcfea02fc9c31776dd95a644d451a09389437511378450e989c6ab2437d3bafaa9ec43267e8545bba09496654ad4daf2d00a2f906484795538589e67f00e6caaad27a61de31b04a16b9b6176093a1cd12d5b2c55fe19bcb36f2d8e57590b7c48156cd4a5e2a453762b6446e39e4b6f91f30f38571b6220b3f6350affd1eeadb79288362ac8cfc264f14df041298b912f4d605564996bf8d8509a35eb1e5b408f65d0f79130c7ba987bd3fcb3dbbe188208df0d1d20627609230fb0e772bd7561ecfce8af16109b5d847037d457efe6e2d4b668945623fe8efca23398fd1b92d1dcd50480a04d9eae761d577047fe113997b7ebddf99ca4b1be477903b57fd23df73e247615c9f8e9248012531e23a8dbc1b9534cf164a9e146cd16cbe46f315b94b8ae6f75982accfd069dc584b7f853601ea3d6b943c2597406849d859f350a9fa571e834690a9a069f22ed8a6386e062b0eea97f7c2329979bae71aa659b1e8f62f8a45fd3f21dd42165929ce0716d4ea07e474812aafeb763dd49704260220dbbd5f8cb9b5f0334e2311f471324290708fe7a0feee27df07c8be1050efa047c0d1947c1e7a0350c2a03feac94b80a31ff9fdd093f5a4bd9b718a38e225b4d84711355ec35e3bfd90228f7839fc9b0784f20714f1b5579d8dd74e4957a5018bb342cb423b6d82342349ae321980a88e04539b08b1405b6f59ea75489da43dbc696aa390e7ca4e0d4e15f03e44140ad2d983e82a89a1039b81cec9684b6a271e549612a037299a817eb396363981399e7ebb01ce58fd362b0c3a108dd60632c6e1dd3c60d6d6424b5ee3458feb0645b57046f56cb3b192afbdfa931c3e97fa50ce11af54fb33b1e1263afd1cce66b840ade3f47576ab64f0999d27f7783a3c9d31a46343c380cf0dd80fc939f54e095aebd9b3272dab1cfa6736e849da8012fef613f0097ffbc5f0097ffb45f0097ffb71f884bffd187cc2df7e009ff00767f7eeee31ec1e065cca65f71018f717bf94bbd4c6ee5101be0d63b6332becc19eac5af984bce83d8ef473f7f7d996a697b7571aa63265d0533376093235da52cfcd4f47cf3432da334d0affb2d4e876b336faeb53f3c2fb0862eafdf0aeac35f9ce30c13667f93f2b1d8a3263f52af128264777570f3d01d64506153b2942ec21c8b6875e2ec6288b4a443f4702591af40cfcbb00aab12d2353de9ba79cd4e511c64448cbdc734040ff8b3ed70fc5fd5f38c1981734cf4a85f739e51b259bc3989694541491f03af76b25b38316a2c4ce4a13438aa095dc3018c954a833fd90a5086e94405f4ae97f1c2803ea80328200cae9f03789f050ce81cf2002578610f8c4789d06c6ac1e8265ba619173de7a97a28f7f4b73cd6914defae634f16fa641707f4e2ba2154fe1a49e9ba9627b2c7e0045b4be3c55b1cff0d682e914caef0be90e3eed007c9ae870d717d27785f201771f84f1e89f334ff799d022af67fd7c6ce1bc3fa75d9a023dbd90ae1758d2b058f954d70bc86b5646885f10eade95ae21e20f08be71e91de97a59fca76e4ff936a39df349aa4db14ee7d46991b6e81b082c20bdecfec58e6f5b8d7de55d5fb65f6efab17145dfaafbabbbd675bcdafe7eceb49d6866bd545fb2a1ae1e8d9efe690f40b954801e4b7ac3114da0b3487f4ca35b802da0c1a849b2dbcb8c87342b18617d6207ed45841144b5c79698c45df4a6bf60cb76499315eb7d17d15d15617171c74be8af0daa6b4c9851807f47915cc62430e51645a3901346fb13ded53bf7aa090d5dec0150295f521392d9a68ff2c0caad91a3acea2a8345ad7c6a2f38de77a8741806b4f627fde9d21f0fea4fca80fe9819d4cafaaf941be94d56f0a3714eb2e7ebbab0446302936da113a15c435c957bcdbb9054299faa04db7a75da968df19bcb374bb6b5ac8d5f57be4ef2ca8d37e7be55f6169e796343342383f219350a64e9127471d4825135cb6da815d169d44a5c5cb9c921f31a10417e4993d9ad64109d2df1a637ec93c2f7799f5e142ff25c5f2d2ec1bd72ad5e041cd509e0a6a5fa7c8188eeee024468b5f800466590eae5e7ca514ff839f809bfe67f6b914fc26990931423bdeb4ffad88e686017dc1ac49ea3163c1cf47ad6fcec7c9be7fa578de1c2aa4d4d3aa2a22c8dca70d9a83452aaf46f028a76f1d7772cd58663318ac6b8f3374d370df4a67e70d3244d8e365e66a2859a59db5e8a2b4b9bf0de4be7fd934fc211e1b8bd37d26bdf10b7757d9da7c38c6e1a83322b4dcb3b3dd7bbb99f1ccc6bbbcb856835db14ebe9e60a00c7fecdab38bd691fb8690883ad0954feede5b8d9268b5a91fcd27934facf3f1c1ec520c9d26f26db6e9ae87bffc07e3455d737e04fc1b14323576d6cc0714ba5c1115bdad37c5c5f0df95f6d0698a65656586df50804e955f4829f011ae915dc3843c805a53f669e5e65bce6f64a5f7939ee86a1da648a79f9bc22f9066ab91a4591617dc35164c2fd855de13b0fef64c5c40b581cb1a0ff2fe6f660e6b7f1238a1e451f430d396400fd4fd3dfcb8021f907bb3e4986d16f3e047af9648c36656017991798ecbdf3776121709e1f75dd3e21ddd85ed65ade84a2c217eea3987145fbaad6d54dac690b9cb71a71fa0d17ce76918336b49e86bcc32e0bfe6fada216d3fc5e9c58b69e8b13616ce8b873ac2fdc2c8ce44a96dcf8a4a50d5a10f3a7bfd28d1631db301de85428560894cbc16872a105a4b11272901ef73d00af9955f468935891e2bfde774da7b5cc5274c7f756511a95260745ce95a55443018c253a44d867f95abad929651b5280d564123d868d20073335b4ec18f04f49b7464a2c6afa853a8ffe2846253a26c32dcdc87e12b768fc98b77f4a7fbc00f1f30529ebf4058aa62a490180811e501e6db8c93d4f36b4b269e3c0ae3bbb6e96b4045bbc1d6ea9ab79115a67ecd2e96f102dc04ada8803707e5713b413463c0de721654ec93e1803aafdf89c2e989341e6db59897a094b36928ee6962eb62104b39aeac967ecbeebe97912a0585370b7e46615d90426db661a66d82d2d9082d3271ab9e7f1138d0826d8a248639d72a070ee83b83a9e147d6406f48d96715e7a4c1598a852132a261e71a36acf058e23552d124c8eea3f7df9475f9a82bcf66ef76d058eac3b810d103a36927200910de2589526a9da9a068a5b3aad12117acb57355fcb5c38ccdd18f942cbac1dd18c68fa0e8c0e100710a1ecd6192046dafc80ed8902f43e8902e161b5f7067bf3e4887711aeef518e4444a7c0992440a4809a82898ee500f68df383b4732f8834ed55f76be92f9ce1fc8c4b9a503d8a24dfcca27de9fea4c85d7fc1c20a66eda4f7f5f85362315e3664979233fb93c273e831dd41f46606a3adb3275afaa480ddd5be5354e10926bec779a0a5ce9df4b8f57256feac887e871ca7b7c9b0c4022ac51764398576b6eb21b43f4d6f75018ee23fbbdee24b9c993077a34bc1e53ee57a6b4925956801f9f1f4f50b6236310d588d4651a0711c081c370e8c962cdc43119241ef55b47f9d84d05a2f87a9abc635008ef3a8fe9c86b3d276ef56d006eea41fff269e6ebe00ac9213442df4ff90bc5c006c623dc4d822c944397e7c8e0be66c25d3ed1401e54502e5656ddd7d365185598d17c8a536ef75b4a40ef8aeeedceb78a56140faaac6e69be2bdbe736fd58e3d6afa2f4f88786956ac89240b40d99edd7715bc34db9ca7b8a7dd799275fa03cfbb9eb7144ee59171d81fabe8cfa410b2643592ad9216c51a54c7a77a9a21e0748cdcf795fe28c7db2d8d5ea7f355bc6c345f6baeaa345db5136999e4ac82fe8ccc5d85c7f5333de0cc40efe9f1e0da389ef657eaad8740a7a5259ea0565b056d5db495488ddc5901961564fa55657c00223d1ea823dfa804df0e8d6be35f2186806e18bdd38427f4543c07830bb104344a91446a4cb5498b85ef953be1d806bbd653ebc6f3ba7a24ed5fcae90b308771b623534bbf9a2462b4cae9d0f2c1f12e49ebb44642fb4e375b0094b44f9ce221fdf59f70a68acc89f9f83d0e5fb86bb5d9aeb7e500cab07320a688c9a9cd531e7b080ec45799724081b3e09623c3f433cb8cc286e6f8535baaa145c104bb01f9cc69e9c99e0f746790471dbe87f6ade3d6a3f55a0238726d0304d16fabaa3f00f6e54492753082d8a7a5efd4834a13bdb1d9437f81000cc910fbf31a061c379d03d0966edd02cbf22f8e2c617b3240bf8ddd6a1aa48c923ebaa35ea2af2280fe2803f4760213d9194c7f86c04567185c544588f41f31229661c4ca231940f41133a83c9604941fb930e52927a63cd220521eab17293f4ac395a7e422e8718a95a7c8c12db6dd2bdf27df2794a7ea071cb7202d4dce9eec2cfade017195b10ed0db2fe1aca66750a7ed97d36c378c1d224d3d40348f849a5c0fc22d4a4062576d91a66ea08f87796437a45c091a8043de037ba4d336dbe5cd9cff399f8fa4639f13d2f3f8b463c984f4b25663fb54c3a3f3d25126a03d3ea525f9d159275488cbceb1953ba735b9e3c0b254fd4cc320b4b0386bde82b1a3c48354ef8541fa419408cd64d443d9203f9ba589a05f53fa006009683e7b81791c378c4a47c1d9ecd5ea3e3a69ed0cde2a988cdeaf5499fe8015a5d1d21e406d710e46174a5309f3c4dbe9d62a591ba0bd7bc414310aa72f1c12ff68ad32af8de1e5338361db0280f2d86ce8c51a061399e09d6c6f00a86a5b8b29e74c1f14e4f1ee013b7d712d56f45ac92b457715b7ae196694a17d7a66279d2f09d64f170f5619ac6ad14903a0deb83898b0973692fe55322d462b7a3c5812b6f965efb8a23525eb29897990da29c614778b6e516496984ead97b2928df8b726baff02502497b42a524adae1bc2ca21df5e218316b12e3fec9450eff9422a7ff9aa25b55a6140c512e6f00689de1759ef228ac2485c9a96a4bc394c7a66389eef8ade99be38a35251d8af5255d34d902908d86f8441abb62237c1795dc1745c1b13089c5457de9f632e88108a3f4d35a3b7d61ad17ea2b472f23c6509f11fd886e16d1dd58ef41911b713ab31e40fa5a217ded14a2fa354435ac812b9215d715298a66c51a455b514a95716d4c663c9153659a8ed11b2f8faf5a7b0b205aab98148c6d19070a9a8bd26a894486f726049e6f77f7a4b101f1ec6a35e44087fffa22d89b1eae37b01dd14ad41b462ce67ba4873d2ae97bdee68e344bbf7cde46abd7c94ad617ad71f765a80f77eac5bfdb870df5e0b77df8d6fc47fbefb5dff93bed5f1f6effc2751bec45c7e020f458e9f09d0e9c68413014490cf65aaf59e82227d03fcd0cb2bd9f0e5e2cde508568e4686ba99770b46daf17fb27fb5ff74ff16fe668a3eb718a84fe334960be7481c337adc0e9ff4ad1ad32c6cdabe4f5d871ff35fe6dfeebfd6f73749270d44902ff14d1498a458a4edfcd302df2edaeb5559a824d1bec6e5a1bab37d82f9a275455b5b57163e3f6ed3e3c8de2d070c354c6922403ca7f925f4b9a2c47382b5bc8cf07281ffa075f97c4e69f53d49425e32968b717d59a6ad0178662109eb3caa57be8f90478794da5e5e55637148a0632f95502180fdcb6a1d8d3bd8abbb049d2c5ba48ef4ac354c334f309b3f93384cd1c8fc50b12fed6a3b5f57e21185abffcd298602c5b3b776d1883569684f521e2117af8faaff727a138880eec06fd19fd021637658cb08e31afb5df866a096b6063dddc38cacc34721cd94fd6b8391294339223ce338f7204f341168735a5b58435551a76d0c1ad12684d03eae6cc373b7919516322391c5d3929aa65d91e13f8f80edb4362e63b07b42535304e13a3382d73095a2b2fe356cbf37398cfa6acd77dc19271240d2dbb7bed063cab9c231623ee2843c48075d9fa514f14eb61bcc6458a26c0a78bb9b5178a044190cf93e81d4e184bbb71346ee4ca5c7240d42b1629c3c400ad2e2bbe57acc734a707b9151f17299df1933bee3c6033e760da7c87ffa297ce77da1f919932f230e45029c7f3e5233954ffcf473934bc767bbc168dfa48de64dd5d8f4e3df07bb5e3ffc3da11bf2a7c1d51b7cfc1c33c52d2364db31e08efe3b0f9cf950ef7b7d463ce9a707371bde7eb6128fa5f4c80cca5027629cc5def8653cffc3d8e374e471c9ff296ee53774e4d24cc91f0b4d7fc826851fac3b1108b3d981c6e958da322abd1fd56f32d0f0bae0dd398ec89d6471318b4228fd648f5165e3b280b298a61d15a2ad212e87300c42dfa1d893791063de7ac1ff28a34caa26e403fd7083e5a4a2734425fe7d17d83a752321b4487203d7ba244ca5231a07f944817ad41fdcdcf29ae57c44d787bcefae273735e82cf14cfd775f18827fd49fa44a82d13ba41897a8b964e7549a3487510414626b90091f3c7affa3358936bbbfffaee3cab85c4d0daaa7f23920c248394254a44bf44221fd417d5cf77cc6b9db25e591a829dfee6715e0605215e7e6cf37c1d8b6760cb386c39dce818dbdd53a2fef817d832e3044bb57481538cea4fb0b14e092781994bfe64d77fcc608e9db20ed1c704f0ca511e9981cd963d1da3d3fa384a9e7ff9bc6379d05d14f95f342e3472a3ac2d1d5a3bc9b45716cf961d5277daddebd409ad8fcba0ba1e491636fff8b961590cfaf5ff92c5c68f86252cab0941869bff136c8d7d78e46bda1f1df9619b38d520e98231fc0f286e9fef12e42b05f9d3449a218d255dfd910693bab328ace04f5999bed3c4f45fd13e8c403ffdb81ce83443ab49650847b9c531bebbaf8ac852bbf3b34ae1984de8c6872193df1341c8fef82ab259edf8aee701f2ad59a3b1b860151ffbfae55a5aa0d7feaff2ca95368453b78b6df20506a705faf2d097ad2c894d6066431a515af94fd34141bf97129a421b6d249e402719c29ac20d870d9276a17f89fae3d057318a07e597904f5ae7645dcd8391c61ee0bf5af4148131e6742b3de113f1498bb94d64130f16bfbd7bdd494bd1abfe77bc6f5190a6a216ce639ad5d0c4fb8ada3c1841b63b81608f8b77d1ee1dc9c6b660666eca8d64645fe5f70607e1ccb1c8bf059b57785d7f5c3c68b902e177b436e6be7dd8b26b1dbdd34fa6ffc8087d763f4cf9d147a08a6801bb52197361f3498bff3a7a825462b98456349aafd2729b447e195230e3f235eef9cce59f8bee14dd24e6f1d4214f5de17050d2fb14d9f1249de212530d51805f3360b455967ae09dfa4d1e57737acf152b2907568b1cbc4d565a8a68ca480eee86b8a0377037df46ccfbd0eefdaaf75dff9bcb6dd4650bf0d1d22ba19fd3900602499328502217a2e2f03d2c1f1517d549186d60924a1d0999a1485d9e67419cfab7f3db5d7776df2c68f3bd5377fa79a8ebf5b03e8a8a29b48a92560fa80694e3845131855651b81c17ccc98051310ba3620aada2b4d5dda71a54582059cf47c5304af15f9d6e75dc720e78c2a898f71186e6522d9c293e2413dc3345a9549829b468a650253c3a53cc480f6b3ecccde6c16d92dbd30cf4cf30d66a80f35da904208bcddc6549a09e7030423ad31d7d8c2d5bedf85fa2ef57ed3369446a6f9e2cad388fb5c178fa2c825434c248276b72297c172b1a31cdcde48494ca14af95fc6a38d23e4917d23f73ce5483f92b96dc4c2a1a15ed8c56f1b55b9f9036bd0822a02516e6899eb2d47073985bf3ce61b7853933689babd3caed26439bb5dc6e710e422fbce2ad53edc826481ba5edd01a49394f244df1f53b5ae61cf26b866c880cfb14c1210c720e034aa13d5ca57f085668ff0d473f2067ba395ad33392a3e48cdffa6b7c4daede41729abb5e63ebc87aa63ffd7e3d61fca6ba6ba96f3e327e531fad85fe8f262c0da8efce29488e337f41d608d34cd3e37ab4e737bf0d49226c6f991c6280f15df8c4d27c5279981c70dc760e4c6db20471b44d117ccb5eb9065986504638219625ec4d9904b8361eaed14b483bcb52e59707072949cb3a4c63698096a89a5c35f414f61a050b9f294dcbb65cb287bbfd58198fa3d4e1de89acb9278735a15df9ab82ee7c884e63629a6f6c01e9173bbe6c0f371f36cb0f70fbc37d96039c3d7860ec2bbb5fdef56debe5a6a3869982bc80f485c68b0c0fa97928e07dcd7288b3f3a388f9964af8b6b3f221e5e18d8b3e32702b2805c9a213329060b212bd3072954d76c700ac5986d78a1d13e6f7cb0fa1bde3f92a268eaf1fced5f74d163d2543f12e58dc3df36bab490ce6f645df9fd92578462b443365b0e4e9afad4cd7e0b4f6a9ade11d7c0958beeaf5b3197846d9d655abcfa686377eb92391318708f4a75232191e2376bc13f79022762f1678a5a7e43380a582df4b3b6911559100d797688a36e43b645fe85ba0edd6462676e3f4eb97c18179ba5b251b26e41575a2dde006bcd956054b441026708704446ee9c6ab1a60fc230122fa672926fa84009464a3887a7d1ea07dd10efd468220fffdeefe310d03a9f6ff7f5af834aff8ab82f98c36a0b4dad483cf86b81c6f5d1e3830cfec74b732fba56e7cf7fcd910f35963f5ff4bdc9fda96a72f6b15cd96e02f74bc3c57146e84767514286b9ced71533ddbe0548bb417003ac5b6b02bb1ddd8b4a20949d851837e9611f33997395d5f2f06e69cfcfa9279315a3adf29d9369d7e518dcd16c7c594245b4789c1819c03e7f475e741716d02935dcc43ef3bc741bfe714b30410b9f359228b80dea0033e4994476bd404ca0ff4100326e7d0ad44eb81d2895a47bef341e67464f5018ea0fa673b166c1c0c24bb06cb4cf46231d069678b5dea6d3608b9dbd987f02fb1bb5bc99cb5c536fcee58a0fe4dbd743bd7b2711dc678e8eb1e0cdeee2d499ea8c5420ee4d0d94efcf7f75bdc94644e772cc81ae8b44f65e44a6e9fb8b56025a60d8df59b7768dee4855692e867075c63742a7afb779895ecee45a7dae8e4fbb8952c7c403372ace0baff593882e41d9c9188ea7a069f79afd306e11e2238c7aafb0310aeefba1dcd4409d7bfecfbf6fee52ec9ede0b61f3ba0d7689ed6c4ca309cf2f81a673d80a82c8d32c9c1ac267d1d098adbf8b9c5636dcbb6f837cadf90bce9c59de25b84d366e403cf6cc48bb357cf349419db6c7aad4e845f53e298b8d6aaf4ecc4fb93ced47cd99add4e492270374e7466625bb3084252300ec02e6c8390d7718a2cc569aa793465527130709e9b4b198c20fa1afd130910348ae6a834180942e8e86b684d0e5aeb47b072387c9a3d791cfe6a8485250d805e4c82cca49335d1068996d6920051d9cbd1c792a564b811e2f1284b638d723093c3c36859c96e093dea9227ed28f766255904ed75692c7dabdcc3318ab8837a0e7dce0e965884435fbf78a28d1ea5c168b044e41857f72bc28de6cac9a5fc1ba2b713f7b1f12939f8c2ce8d231c9150ed4c836383ebd78dd229eed38cd77fd54eb00d97d3afb8c07069e92fbf6acfd43c63cfaef9a39d95d448745ac7844b5d65ab59d338100de99640ba1dbe755dac298c4b8f03666d28848c90d0e24b6359895ae278f252d3a1f813e7e9174940684fd6c8b4d428c89d12a7549a4cbf44405e7d8d4712a56a25de299ee22f58f423a907210e92f6b8349abe510e39d621a1b16e4fe877900e7977a3bbbfa8978c5606611791347109875c83e597aed5c2f6b0507d3c1c6d827940cdf003b7f77a104b72959eefe301a5bf43cd3e276c5b9a3cdf46206b80ff67cad41f3e7d50aa7697ce7eac34ebdf11fe17872299e413c441423bc51f6207d2e45adbfbda335cbb0ce4286a15b6a8be6ed36bc4509e511a2f5e6620aa15c9e8fd4ceb33ef598d24a0250d8021f5175c83ba1624d71a5bcb6a4672a2fd926d42fa99a639dee83ce1d4ae69f767f69d6c3cdb7aa13dbae95e98ba93f709ea0f6626461ba699fea7c2e989a9c6808e315b1e5e89f573305acecf9fe42ae7bcd8f2867254c6bdff93fc0003b9a5255abaa807102a2eef03e70775b621887f906518e84f24e25f56eb54dd83e8aec509d3bf545d9e3aae85dbae97d5dfdc421ebdf2605d29f72c6f2e45255c9b53c87731d07b8fa74f7b29dc58b67a076d21d07c9ffa9130f6a7510fa0e68cc0dafd5011475b7bc42fabad46d720e7e14fbebc974ef81a70341d70eee59eff228b86e8ac208b3080d680f8f3bf9e7174800d302a8e8a3fb8f6e8b4c76dfd490786fbf52fb20003acf46b82bf61b609d2b2c48ef98435210e571a368580235e1a7eb6562fc1fdb0fc48a210b769c7201f31247249377e4f058ee8f21cb72b06f89527e10cb686ff899e324a85f99dc67ca88cfb634e5af4f524086e9d668051bf30f757a49fb48435e93f22c161c39cbf3ff5c6f4ad53d64f5a13f137497b76d1a456849762e200bfaa854e99859b10afee858223f272745aae7c31ef191d582cac115cc34a79b8c3a6b3460e8e2b0f4d9cb683af615ce43e658f95224f6caab99291172298d414defffab21d7960d35a4f1866093e58cd95b9ab2f0a50042dd46fe07d30990cfa607b910fb58efeef3e58d0f78ffb60b250e88371f56faefaef3e58f285ffe483d59cffcf3e58cde7c33ed8a3e754641c5f679a8e1aa71a2b052f33b5cdbd129a7c87f3d8f8feb6fe06ee861baef1d6301c68791cae79082eb96d043ec76ff0fd3c84af6504beebbfc1f7d310bea611f8aefd06df8f43f8ae8fc0f7b3fe1a01d05a82a49d1d8806b9daa9067af78078570ca10dbcb205047ed70cac57ba81b54183b183dda4f5521dc83553afdfc702c788007bff3e16fdf66cb90dd03b7e11edd3300b69c68ecf345532478d7cabccb560a340d18fc3ad967eaf5b18b8e28c9a968c46a7e4ff8cf6b3de961e2dd21f9702a87ff30e5baa886ea0af2340e0133300415415d66155ad1f01742ada71ae79808abd37486d7912509b28c0fa3d05bcdae8bda371733c3dba01ed7083069c89a7c735006bd10ecc7a6547ccd1c28083d622297c97c65cb2eb26d5d91da6d103270a9547fcb061edb9f0e4d6a6f18d6502e5a99f09dc3a441171c07df21ac6b902b77273bc10aff63811af688c0a841ce239b50550033ca7223739c03ec82d17d69f4368ab9c6900f18a252270c439e853c9cee02cb111276269e2cc237c23be73f30dd40ff32da8168b652fd9d4e6bac7b946412e1cb6d032917866a1fe2902dd31da331354310d20f2f227e81e51561d08344a8043d33230c25e780ff7b7f9bcd0dfbd23fb3badf0397be60c382b2657e0313e320d3a6b37cd20442dbec18c3b52a588ac07d959136b2df57f45762c86ee41f092a6e1b65a64423b12415a0bca929f5f82eaa213974a5bc59aec2cf90c0fb46e15939d155a67b171e729628ba0bdfc771a6f87a32da210d3a05e553f5869c07db3e5d86e2fcdf25d42ccfad0e67a805b4da4b43aa91b8fd28c415c0cb9918c4ef4572575e17353221b2af02a57055ed488694a9a782b8a6c33531be33395a12459121d3a33fd35b7befc625932ead370df8623b4fa127784c668e9db158095878228e84d47172a62b8b5bd5a84357309c4aca5ef56000fc2068161f41e0be7849c80faa25842b3cd5a748e87599ebe4563143413bc05231fb14ee3c1f054942ec0e2e5fb21d744225f4a0abca9a444353b2e0ccabc0a9480bd7bce1496ccd51f24faa82d3d98f5b209637656ef2907552de3b089a4b2f4529f075900665af461649fb5c1823139d5b00397f29441ae3e22cc315ef270a709d346117f5834317ed762769c0a54ee51969e18bd2b9650e5c3fa273c1d7ef503e5c9fced1e8aecc02b4df4ba0ae835778cc95c42afb6e1d97ef20ab452a8f2e8d59d2c2c8ab149fa40ff388471275969d9925774cecd09e8c1e1f49d0ac9f01ad1c2a6171a11a6a986fe247a8d4dcc925924369f7ebb1baf344479fc61112b03a24042c671f75b4b492c1777ec35015a5a88622e52e0988b98ec98f074ff881d996e9d7678f5165f17cda035a77eee84f33de8ad502612b74980673039d3a20c82febbc5044eee91a5c154300994212440f72544da34b59fc346004f4ae22d46bc39bc07ad9a112ac7f8fa01426bb9006556d712cdee69005124f09c69b14982201e6e654d02e6a2b8d972410a58e3eae7294990f8844517af3c02f11f83fe5f582de9d87b6100d5807ebd64a6e5c5585d58f1d9fea8e641260d4a70d6bd0a6d04e299b7986eaec0918f654d5a02c693272d3bc92815f03c096589894763bd240fdda11ce632ac71b302e07e50e23cb7e9a20b3d876491eb3b1924ae323971e4bfc8ebd15a8c495b697907e2dd05c34e0e663a65d18ae845a4c7d3e9923e74b326a163613baa7b6fb23a02f18d2a6800330ba2c820106c898b154d26812e84da23c6b17728bf3028410573b1c9ba30df73149925dd14a58dd894268e70b7cbb7a9551fe5da2ca265f53bd2a636e196b2541bc4175d1057732f431c61463cd871af828cd8b47b7e843228041bbe79339541376fa6c2b1ecd4d2372a46e382049221db74b3a0fe31e693966c531419b748f1975eebd55ccf9fb2b350ea652ef5ec50ca5ae807baadfd36772a14dcb6d60da56680abd64fedee94069cb67e3354b604cd58fd4b864a5351fae192a1f22d28ed7a7ea89cbbb577f7f9a1f24294fea570a8fc004adb0b6d3649dca29f976566580b8fc3feb8b6f7e65dcdfdfcc7ec2c3edf5a5807d01eee6d9b3b7d096ccba8b3bb53cd203de38ba1b26eb032e39ba132028bc9f861a8cc0fc3325287ca42b128b26f3075a874064cdf1f5c3954ae81e98ec19543e54b60ba71f0c050792a4cd70c1e182adf02d35983ef0f9533300d06df1f2a2f44e981d0a1f20328dd173adc2b946e9c68e722fdd60adc3dbe5aaf91e38ba04be211a64c39c28978b5eb0292740ecf63259f5fb8cae12f4978acc6c55e6e5414499ca434d40aa991b28152c3b28152c3b28152c3b281528fc80687f5d9860ff9917f04ebcb8f607df911ac2f3f8215a578895932542ab432542e48d850392f51cff33c4d7c4ca22eb79de6f31f93a8c447242af111894a7c44a2121f91a8c447242af131894a1c92a0a1725e82560e950b123454ce4bd081a1725e42de1f2ae725f4fda1725ec24287ca05091b2a3f8ed2f7270e950b1236c43d5e7f8b4fbbd3bcfe161f8ff161a0378056ed057fe5e3c91a61eff9c96006dd30d069dc770cf6db58b40ae4a890209f27900c1dbcaaa51b9d38da957282a87a15089567e6118e28ed15909937c1519cb3cf397407f71bf79708782942ab378a862f6cee14897d611f7ac7afda902d6fd3d2ce8ab021dbd7ccebc690e55b9c6b46d2d66cfd224ff11dd20a3e779f19c919ebcae86bb60de770fd77fcc0f1a7e485cc0ce8af92ae2791d4f5e67d9157f2ada0157f3d6dae455af28bc6ee4e233985753b602bbface5ebaf386d46f2c620791bc3d5bf166373972089d340896be6724e9b4b5620498a223b07bbf9d1f82b778fc7457640bdf94518e115282fe6119c3f5f9bc195a19228121bfc99c39329bf24d480e38b212d6778edbfd6cf8ded9cd861ec53cedd3e8e604fcdfd2df653677b8f8fc47e6aee48eca7e2511e2a432d101857e342fa6977d9c896ff79e1f9e3a8e5b7b4c32dafaf6738d8bef8dfb6dc7721fff4c896fbe247b6dcf718f6f60b1338ec73168ce8d767329e9284980c948b30a316382a2f5eade6a8893deb57bbb536bf1be2587fee8b2a3e0fb574565ecbf22331473b0c33a53eb31ac95cbe966e479e052f73a6f3c33287244c383fe44768f837252ed35cb543cf19174a7ca3ea7661d0233f38f2fccac226e82113dd1ea15f98b534e8f6e4bef0331fd3f03b4741e72bcd090cff9e7cae9259289c5b2d9dcfed18f23b54b5958c1b06e82b0dc16ef89a4ae411f2f0f17c5ecd4758e8a3fbfa36d857d121133068b3b4d4b65e60fd3e0eb05d5d2242c688035ffc003cb303514d8811ddc76d3c743d38afadf90d34b4dd62a815f3bcb498c6da5081e5e638729cf785b92b03c60b75a4fbab11b74493c598781e98e7c6815a42789ed98185225c8418b361021ff7dfe62c4b7bc51437e7558779ce234b13dcc8f3df1d858dbcf714db476846a6456d324d1d4fe52226877e9b0cc334d71bdc7708158b904f686d9083dc1c4615b9a41b60a11b6c68fc043ffb3eea7dc95f4fe7e4960ee796f6f0b9d6062db83a023aa8db9d4f62bd23f24befb9f3ebc172fb08f85fb9fcc5d68628b00fb63f7b49373e6324be0e77791aa81b89ef8e3bdf02aedb08c1a226ffe86ec504747699900b7e40b97db148ca030eba734b1bf85c4429a342e7059eb7b9cb1a2fa1b253da5e6ee40f2ea0b6748f2142ad723f80a42210ca049c95b892c0250cf842b8e15cd384f27efe2b8a57326deed51d3e0f51d46c73dfa273e791d83776f7ea0e478df6ba3bddc8a73bdde95ff834c48399edeebed7032647f715a6c9af17faf43316cae3be6e776b5ddd1074c363d0353ff0d0a7623fb6fd16daf91874e36537b4dfefe0763d060dbe77434f1e01ed7e3b5435c2727ccbc7fbdab53b62d1a9427714740f460fd9597c597dda8ed86813d1cc59b817ac0d2ad85afe5766ade5045abfed49e1f351047eea5b29c48260a2204cf1b95cc84194d2c254db2f0c1c517d1ac1ad4f0cafbda6e94f5ad0b74a4ab468e4d81e17a0f35b70bee5b4e492589dca5caf585b7c4eba21dbe29eb1f7e5f8681d7b9d038a95fd0dd2d4661bee47255dc284f5d3bfebb4b30ae9c27a10c890203bcb77e595042267cb4f0573d1e972fe54f917420dd48ab2945cc6d7ac7b4358fb583aabb0281653a173253b62b150c8174254df3d287a8a00fab07918db23c19b07309532a81514c1989de3c3872d2f76dad0f762ace21383e88e7fb839b8239011835c2ddd5d815bf6a05348615b2909186329e4d6ddff2eac77e9792b99958b95166a69ff1e1cda646b8f98af61dc424922841a9a3785fdf72cc1d6eec44aa11d2fec11b0c76fa6241b0558e675be4f4ca6701ac08495ce6c9ada08713739bd71b9b0ee1d8ab1d196008d28ce6d25c12fd9a97800b5f4076cf692e378d9ea40dfe9a06ca335a90e50f75d6302f7d4039b6b0cc87cdb62e1eaa74517eba36460b6a44e1d98180f4415f51e986a9b5559eef4e0df978f787f867bd71fa8f7a0327ac6048e8b07ba50b6590394e5362ed7671fd2745d6877319f438ed285f6162b0fda3c3ed7224842fa79bdb28cc48ace45467d0d02e52aa0c953965678c6e429cbe114adba527f45cb7db1464565b85c57ea8b1660aa746bd16757e058a27b8457606cbc5b5454cf9f86e17926a1294996b7c0df549e679a75c26dc0cd58696a287fcecab114f76387642cf4a5e842c54ae5619b94fbda8b9cc0d12a19334fc7ca3f8191b6f4e017f43e3f922d84325e84ce644d5a72b848542505fa49b14037896d9d0494c114c64ab3f0aa1d72d1d922e5479f00e5a92b00d2825799eaf140f17170928994dc07d26f8399694d82fffb2078359c67b95d9be6cf8473690ff81361eef360fbbbccf31cfbfc1e0eb7bde6b947dbf67fbced23b0ed6357008a94c153beb1d6cb3dd8ed01452cf26c3ecd53706b1c313e5e9aa155ec539dc84a04a1b9c2066b14fd6fedd62d799933e6fac5e42fcdddf15591963b990dc7023dafd41768a3c8dec14c79efdb05f5df6bb7e4c1515ac0957c869e2af82c5ab00d8e5211972afa0ccd9cc2aa1fa78dceae9316425be4e0fb77ae869f7fa70ad141d0b3c2de1023fb54b0c209829def1b5e2545a78004ddeea07c6780326871244d4367774f0b36f06e9905ad1509b607a6507d94c39f8f0e6b4a68e4f6fd843b8a208e8554ca6d9c5fd61e6d61e27d1d7caae266b4c59fae34d04f3600f931f4c528c3ade8026972409d40f572658478e84469560f5af54d60ca52f0b5fc0e5370238f47d582a80db62846505b7a54a0cff1287da309815f8e4acb0e3a724629ba9f4f423f3649d1ecffa9e2b6a213dd32cfce52a416bd52d25cd456f48aa773d72dc5ca1d9d0ca9b80eed64aaff5a457ad14d5f67411acd96832267110d6be099f222c70e1aea0bb7aa66acd069679aa01553ef5a45ef3d00b2e52b353ab6ca548147122e889d515d9738d8fa012435c23adc07c335f26fa355e1ebf16895eea4658703adca293f22015a23e46abe57dfa7d37e6147bb8398f67fa29a3d318893db938ceaf644b9fd0f61af1ed92d44238346058dcea6c9eacefe2437ec69873b862ced9a70dd7d8612b864b787ce53f60ab2f2501f9505f0dd8ad44207a355342b9f0ac574716ea82c171a9f30065fcbaf5dfbafed7fd131563508f9d6dc66ef7ff10b9b70e3bdf3d13bf18d5d6e0ca5ed01754377fc9f425f9819e921ea3bf99d0d74aaf45e88ba53a0b373e8ab34adc3d407dd1aa61edc11fcaebb6eeacd7711f5e291a74479ca6ef9afa50b9b00a2f8926da8fe4dcb79384286b89d2529fd8343bdbdb5a177f80cbf7b2736a1b1d280b82bf06be83b0ce09770834fdbd01ab06023d42d8a35b4bc1757a4f4cb154df4bc0ed86b31ec3dfc41b947b4e16bc39811f45d1f3a972e7c4923e8469b5dbe07ed5fac1e8bcee008769712bcf3a04d68b79d97f713fd4ba24d878dff2a6ff0d48571bbd60fbe8f2dbcc168910e2927bb8e71b37c45fd316ecf19bd7f481e45725582665bc0847179071a8e0abbed366e3ffe0806321319cdcbb168b71dedf61e35fd4be51ade6d8f2d74c88f23fcc70f734fd5a5c35c6dce1790fc13edb6676749b574713960780fe1839e7feaa03fd49f74dd8ebe41caef2407376e4223ce9f234ee36765ed3f4a684a3e1dc67c272d3e6968cd19cb2f4b46dfacbb12ab7b074fc1e54bf298d550179f8a5c72092f889d5548c11908638bce61f957623f39c75b3c49136a45188fea11df6578fed1b2c653b26661e4e911503739a8212e9b4afb13673e72a2a1484bcb5bc43fc7069a5c8372d4bf909677d1597edea76a78977b569225433c2d234b3030dc6ef271bfe611edfd7508ee303a41f0a88e6fb1f3b302aa2dc0bf8ba2d9f0f6a9add39a366d1447b8a511ed7b95a5cd4dbbb19a971e63b7b05fde6369e46c7d53e612213a6de367b6a16f453a396b2e47a739554ba3a1cdf5200a68bef7aa8f8a5b2a2d994965c9451c77a16db7f8bf06ff6bfb6dcbd38d4d658dfc79b765ade816fe8aa6558dee7bec35d7c37304dada28dfd9c2dc52915b6629a22d1fa39dfbb61ccb7819da775f4c90458e489319f8d27481c45719a1c28a68e8df8d7709dfff00575971966795b8100fdfa95be0f7ece58230861925b4f33e6b30e08ea29c4108835789f7e27205ec89e7ae344b20c49e336afd688fb092556e3d7db7dcbac26d51de95fbc9d0fee0ab61c2b7b9c03e3ea72ec39dd368e5730ea4bb736af65ae4dc79dbf86fe19c69c2f854457cf45e65c41b98309f66a35e8a669809d43361fee5f2e84d1560878325b3e0acd18dc3a7274b9841e4a887f8b2c244779bee678330df65a159fbe3fa85058ae48f5ed34799093add2596a494273f7ac60e5fbf52b02afaabc2bee15ba8e67197823ec6d5735c6fe841dc3d64ffed093d543b3b0b7d5f047d5b049d9045a730467e5f44d0a8f3686616f0ff0de1a7df21c7b3e343851136155e867a1b6e0ef494436d3061f4584f09cfa3b4c8489309f8d3670cfe34718e4915e4f412cb145608fee797421bdff2d2a8fd13fafa50b0a580f6a6773bf8364daffa3a102739cef223f29d057a1aac2125b7caf42256126b0980ad99cd53e59510a7c7ce549d0a7b8e5ed787b39e50863c8b45722817d4a89c70cb3f00fabee09a2a722708b69659a31bf551a340097dc071b9a03fa9a42678af3eca03b0d0333df51a9ab7e19b67a4ec073cba7861b17ea60c4009bdd70088e778bac8b468383ef9e788e7d0098f75ccef7f8b049df7e05a794d7fda84e9cb7301bde56770b9a0a066be9d6572c15466aadbe7cdd09ff022ca52751729c64b24d8c92d65a9a2e3b984fec42840779503e5917f00b72cad2e5c58c84335a667a7e94fe412bbe939af3a9c3dbf12cf85da84f14a449cc39e43bc3b645b9a7eb1d5d855d651defe65d3b78d897dcbeeafe85ad5b1ba7d5debabee9b7807a800f758932f5e449acbdbe99788fae86246ab8ce805e81b402d29f16bcbd74ad6dd7863ee9b656fe26fdd583f7743d906f455a08b16cb07686c429e43de5aa42904f3a51574e5ce8255a736d1371b8094f6a461cc26ecd037eea6a09e471a76e202bd0b234d2a4c340b7aa6dc98cef7615e89de05c7115f9823df0747d7ebc3236cce3f3eb6c077367743659564afa8ca64045f424a27d617a5c5886716f8a72d2c2872d2be50f2d28a9c682507695d95e9017eb160557124518947172e2b54a496bc56d2a88c988f9524c3be7aa0952fe52c578a978d0ef000eb8cd79857dd5ec2c1ccf12beddc1c316c7d59e883f271a5a6126a44f16702fdcf4417faaf9cb39d21e70c2a56f2967cd8ee96e6b96b69e7a25a85e78475815928c63ab53dff3354b75f8eea9eda5eecd8942c8e403f1ecff274f4bd13f4f5136481f92fa01cced9141e043297b24b35226ae9a780ededdd8e85476e3b04d865dd223a4b86ce356ea7737b71463b5a764d8eee27b0e6801a6c812e2ef4204dc8a4ee9bfaa5d958a895640659d9d7359101bda0ca7c48dd66a74999849543bcb28eed5e0b6ae31c75641fdbb04094f9e29fb258976b7be4e6cd18fbd343d1c7f5146a47ef9410dad1e215f2a5bb5893bc06d3b2e3fe007e78bbcadcabee1ff7b136f2c21b18ddd2201162d0a29d92c37b4ae61eb6649b74f38905a76da815d9827d36af4f86be4bb88da72ad2b70b9d86ac31e76db1c93e67e5e701fd2589be73b2dd4b5b1bb7d6aed312dae7ed282dd3a6dbf8fce76d949f01d0af9bbdfb0374f1d48b045ef59219ef4fd2b16d79ba772233ccf8052612c685c8c7a0df22b1700bcbdc1fa4d32aa0869218cbb40e865b22493226f225131e9961c2ab8ca4889593f85c13ddd7002a2d1409a19d10babb015aaed641985340e085794b7755457580e59003bd7fa697748ca6323603af73322dba7b458b6f7a7a69e9d1373d687f0f406574a1b344a206e023a6e49b017f2fef86bc5f9e096bcbd414d9b8dd2b8bfeab0b98619d07d25c2d2d7920a5090f69e68b2c2c639b5e04994bd038fc49b732f75b39e5db235a9e1b935b5cb3a9d417fc8733d9e37aa1a40480bad389c604c33213c2ec422b37b8cb97d13ac6b9bab038a26c85b1d9c6361900168778996e27e216fa62f15eda4fed1307268af3ed937fbc6d9f31c0977e684f1d60ec8476df2d76eb66106cda7756e8eba89b52d857d14d69b009e914bb15f656431b4e406ec581952ca595032287d05491dd6a686945f4d91e295ac742377fccef55257e00d0a8474ecf024b76b19087b9e7cc3c5ee28118d23be6018e78c8f23cd4231eb27238de57481c416171b0373879876d0a019da7834d310f28d200d6da824d4bbfbd6adff660a2f853fb4ad80baf2c9dd6319abc49b97c41adddab4668037b80c336c807c02f8e967800222ea034346ec3805716c2a7d3ae3d1e1ab7e5c16d1be20be201165f1bb7eff470eac3e3bfafadf7a0b6f6bf48416dfd7716b5ad77bb2ebc6d17bb146aea181927cdf428a4a96f8bafc9afeea264484fb104bf83b5e787eebccee3f581825a5a5500b9c3dd933c7d9eb59c0714a7a331098efadfd7d1f73fe374d4f0fb3a7ad10235541379611b46ed9003baf9928785415a7a60e5b09662504b193bc569e981115ada18fdb8966ef88d96c624ace4b474bea0a56b052d9d3f82873109bf27cdb4d8036c0a1aff1fe439330963d9cb629c5d22c67b776ddb154914e0910623d4d61378155388b350efcb0a0f17d0dd06406871f9c93d992f62bb235d06b0321797875b285f0bd0ed4652b69c3d511093572e8e6c6a029c362e23a56c922f2076405d26efff995ed4e1e1d665f455153aefa604cab7f9a6842efa2fba4c4a055d762c72fd0a35cecc49b0ee8198cef210f3ba4cad5b0664b685c6305e2709d758a4edaeb1508ac7baee63714f733a19bd639ffdb95f278ab7d99fbf76dbbeed575e03bfb1a50e6c784c03390a73380dd4731a58fcff8d067278198efe3ca88145bfaf81088ad3402979835a170e428fff170d14938d54c6784103b936749c069a380d348ed440844fa7fda19ad7409e03d7ab86a5684bf53d15d4b2242a096a999eca706d67258d7f6ecbcbcca72fb400e2eebe5b281a9ad0067363e88b2d38138fa9be1d471111b197f202f605920d3591972a00da55c742cd5f31da4da572b4daa3c53457edb5dadbf097b9a4dbe68e69bdbea07c6144ba7b68d562341ccd58f37b1419113b9b70a9237d3f0054b76b7b7fd2bff599bb5ef08bc91dfa0a419c4e7bd536bca2b398201a11e536320b042edd82f5cbdbf2a08d18a353595fecc6282200500590b35b9ac760bb7dea2842f6d456b14eab2ba5c7ca70a8599f0fad7548ace412411777cf9acd54c0797c624d14a906bd798466a11fba6bb9553c7fd73b2445746ca7b6746fa7e41a51e612a47503be4b7335b9c5e7509ff995117e9d0e41a2f9354b1499d40d65dd3526c0715ddefd9082923221af0ae6c9196e5f21b892db879c4d54a8d13784d197c0310d7feb476411fccaafd01a5b8c8f4e23ac2071abf6ea52619dfdd00e7ace86fc73d916d89f64ceff4ac47cf8b2b4cae05679015a638cab9cb00badd67819ce187c9a86d770327d59f974500e7dab6c4bb3cd5d4f5eeeaeb7f900aa87d636bd0cbab86833bdc780f330aa0f7888900f26ec12cade333cc203d6321d5c84981971b6e5ba6d64ea07fb8474743fe24cd3d42efe8e84e0d7d594d0d3bdc38dbb5e896624edeefbafea513792d17d01c53af42d3bfe3be8c3df459f6628beab0c91004cf3fbf64e5f418c3a6ce6bef1b982148b9e9680ca9d997fd14f3703fd6c19c012cc59ba102be901e8af2c40b166a7384686be07286eededc334dcca5488182cb7b1f27070cdb22b76a13151d8cdae791f0b253c1c546a7fe692eb76cb112efa68e8b55118f69372ba4f88724e7eb0f2a9894ae5f43f29954fad9c78d830cd3c334719a10b5146c42895739e9fa89c9e3e11a627b60cde7c78c75569164ec9c728564633671a798cc967c25bb3dbb26fd33b65405f2123f29d0b19be24e8e294004238fd92d5adaf106ef2370cddce67c41ec20dde537acb635f392dea02a886f095d3835d1ea8defb39c5e74ad6bfef2847bbfa355c9c54e3f5e9ff89b966dd922901c2fbedfea4e876dd41043759cbf65460f4be1689c81282d1f34989086221727c6e3d8e21e8d43086a0b61f8d09c6f2a13305357f096fd7437e89a66321ab52f5d3754ad1d39852ffb42e58340b0bd6cfd24d14cdc626ea67eb824473b0a081b756649ec984dc0fcdce2a197ba633bb37fb129610bd2bbb3dbb156fc4fbb29bf0f60403452ec2f55a31283e475b9dd26a8f901834a37c99a18c988229a7bf8c5db69ce93d9c14e389be4f77f2d23e2d7dd50976fb7034f1d6ea0a4d112f529ed8b81b69d5a6242cba9dc3e678cd1b3cc792be603f8979f42fc73bb0b8430b429fd35f24017f2b8d95941294a414b79a3c06024d2470dc6a89cb96cb4ddcfec0afbd689f33169d1b20c4b9b685c2773582262a52a1243409b2b52fbccf5d92f5a4626d74e38d8cdc73d969c55fa13b2bbaafa814b11a0bc10eeab59d1e6627fab6fe7eb2d3a32cedb81d7d9beeaa4daf1a05268e8aee70349003e8065d74079512077ad1f7e4f11fecc1696569fa04123024fdee455c347514a42b07b0e346e3f4771290b9c2ea351accca896ed2d867bf5d2d6248fd45d760b5d71d60653cfaa79a1c5b4f0f4437f9d974cfd5c1d6c4d203f6b234bc03cfb0eef4e867c8d9a65bf86cd34d80eaccea083479f43b36dd1ea8859006f0b1ad2c4d548f66d02c09edef026569b07e1043426c0893f899a1f21aa11c95c112dc0795634f43fa73407417ea1de2bdd2b37334b680790ed362cfe916e89ea564ba31278df4621b76c7288aeff4b09a2420fa3e76c18aeecadd67b786009473a643577f36c3ac82391971d2335d1fdfcdce38bbb5dad8a0a6977d2dae9d27df01c7492abdbf6fe1e125ca084f6ce1d640c60f5432d4184fccf1d59549cae93f83a37b4ef4565eda17ffc62efaeb729c979993f785f5976301b5af79abc3e23390e4e8211d87ceb2924e0f65c4cf808947b5bfdc1ae3c9484ef41e4e44dfff3b0af1d05f5f780c4bf21184a571228fc598f1fe39df0084c13c84c1fc9f30740896f49f0843cd040987a13c83950029bdbb4132b2b731f3fe9ff6de04ae896bfd1f3e936d121605230404351871a1ad55e3be50628188556aad806b0b0e8b581565116d4bcb164258448c8a8a0b5ab77a7bfdb5a61a2b95c50db555ab15b0b65a34026ad5caad082890f739331308a35deef2fe7f9ff7ffdee1f3e599b36fcf9c6532df73b64e3e1054a0a5df9fdf550c6d45b8ac812bf66bf4dda0b493aff6b12eed0aebd2b2a9d4ecc6a984ca9854c693461535ab868e774830136fd46d1c6fa1d019723d24432ac4f1de71578cb88f46151c6d3950d95bed9543bd63ecf2e42517e158510f1ca77e85c02130d15420723f10a25cfd00e1a7173fb18a115f13ff68c1cfadcace74a97e4297f05be8f0dd70f818b46c1cd61ea23fa33dfa44016ffd45fe785b44f9daa21807790fcc24c4cf8fd23654a5512785509a07eff7d34ae31f3711fda9beb62b4dc9f662a19ada5abf12e27480f0c4dddfe0bf20a582ef668b9694c2bd1cee6d6d519291ff922d8a30ea13d3d1b18a89b6d20f6ccd7ad296009d1d6d8b4aeea68dce46250f32252c4ffd86d78f309beddb28088c7818814f5563ce7d2afb840a66f88d38577a729940a6a3f9f167a9aa1082b9979c65f247bc910bfa9fb2973fd21631e563e605353bf6df3df06076a9464d3937f21e465155e40b621c7cca743de419737ff7649251ffec691bfd0dbbfe99187a15be42dc6023251d44981b40a5dfc34faef0136c17de1fee878aa07eae195f29d4f4461d23d5762f1dd553f8323d82e03d97d8d1e705a30afd1ebe664ee79850a3657d084cd9e4b65547db42a894b64138b7c41bd48667885a678f96666ba6e9a3040e4b17a50c2c7994da7a6129351bef19d92e534e794a97b0a4952ec19a678356682eb3b3a4b2ad535dd9deb49aad9f3c463bc92fd645116cbd8716e036f0ba39248bdd5b7e85eb4d8ddafb47a6e78e3240bca271cd78d6cd84ad3078371764e0af07f6fc7d4ba49bb6cd7c2ec7bb767fa40db4e565cdd1c74cac68ed902caf5cf644af95f8b48b92663657c90732bc322e6b166918579fdf8e27fa276aecbc6e1a32ead180c4b4d3307fc92c244954f2f893bc9fdadb4286e40ec9a26c9a9c9346e39c8ebbebf5803d23e0bd2951f716ce6e60725ab19b3f4e8b087f7fcde58c7b1aff9beceefd71d4a4225ed642c85b86bfe69e65078a85d05a3d4f27e21db4a8ee648fe38975b5f87736fee926742052aa7d623e5acbaf7862a678150e52d11a3ee558e1703c915fb1177d9257d71e992585d6196fe3a5ba368f5afb8b7b420875f23ea2bedbaa68195798d16a1ef5a3f4c9d336a9d041206d7ada3dc58f72de8326e2af32ba6bfc14e8a61d1e8fa86a11f48bc5191b54b47ea206bbdd6aea5a752f29d960374ef3a99f543c54545c7f98271535990bb524ff93ee4dedc515877954e05567c6fca47d954c2af2b0a316ee75de4214934f7c4aee14e7aa09a1faa196f25313d2c49e88a89814559c5b8fe0a929859e0034a8e411336ad425ea2aa4f49371d346144fa8938e126fe09124bad82b7b7631f1c6a669aae2b69092c741873ec93bdcde784848e75f6214da7cd2fd5afb78ad5a659a4bb6d71edb931899057d2cbf58db8a1ec048152aa0b29f617e8960692eb5ee1972cbed0dbdb76bd1d2dc5cff79ae4f347a718a9d545c6ebb483b232350b3a299fdfaa711ebbb97eef847540184869c6f320d59147d4c0af119ec3e25a84dbff0d2fcf1af01888fbf8f5aa1c19a70198767daf38de31f494e09d5e3edf7aa16fd3827e4e816ec4e3f098c7b80e34d6861c8e7b022a948860ed6e2efa0864499161598c34a712aa675bfdcc635a6ac7f80888154d01aa1eae2f1a8f1da02a2d8de48e86d4245c78a866b4b320e1d129269ea26b350845be0ad4352d2c36e532ddd6b100d765272981d8cb67c9c860235a1dd87c67e733c0af7db4b4aa96fa7d26d52974b541c875629805897893cf61d8fc212b71ce5bf8618bf7aabaad0be0fb498c007ef08414c3365dbde4e79c3346b0de4ff51f7de4575b9b9a5383f9a690da590d3e905fcd4a8e29cfb3efc0a19b23fa121352f79d7a4d2330a936375fbadc4f1dabf11e3c9261e2ec7e043c2572a8d78de33eeb1144604dc3fbb562c88da68eaf741bfe898476ffe04b644c433f82f9080bd624d3dfec2325e205f10459c5424d7a39ba51b4f5c2ae58e2cf9dfe09185d1b214359ea5f81dc523c683263c62e0d1415731c94d3ad796dfc08e1bd20f9e752f2fe5613bde4d76867333515f41a6e15e56e1d2605390488fa3ef95dcc77113fd756abd4d3219c4faa567a121fcb1309e25b7f19666d03b60317df116af6c134fb8add8de5345bc71f728d817d1fd6c13debbbdcdc8cdbbae02c7af517f6ac4396e7cc6cdf17563678eefb2393e5bcae48ea44aee77c6b495ae05acbd2917295b3c32213e5d1a183b3e11bf654bf4676a48a76e39c684d7ce2fa753bdf95c3d2db1aaa7242393ea7c633fad46fdd511a1da945eef2c515379f536ad36d2c40ff0d7f8fc4ff2eadba573c723a9aec94cb9d8f2f42b7a22dd39acf398cdff49f71fdaa5ba7ab3b45bb9d3522df146b1fd1015d313e0563fa785f43e80d9f594569b4fa7a6bc24153feaee394d9958c8c7b109bddcd8f8a93c5b5e6a149e8d53bd9ef2a84c924fad58c2bf95b8278298a294b4f8786758be8a967f9c1a85634903ad4ca948c573da1a434821dfb46c498b77f6396d90512a29778a8cd2273e4349ae0c2b5d3af70d786a5a90c9c9b67d472976fff499307b6c35aec74fec1b7ac62007b149632350d8bdd51db7ff9cc7318ff22eaa448a11eec441985351d5072cf3b4d6c3cdec9acd9692093f8c4c3cbd42da9d70de1a285b89e7b99901e593cb47bd9f0365bf84c3db31e1bfdf6709fff8f023363c09e1570e48cc58a1ef06e127ab20dc0a08f7ea4fe574b8c32d072ba9ef8e74ce0f99319d4fb90be34589993d7008d5682f089177432f9a2ea2aa82675be63de09b19a3cca2c4250f587d7f0dcf1bf13eccade84088903cda6259a7a9ecac566acc9ab88d72152ec5b3bd6b21d4693581358fd292bd32a22645a9ec14635adb8f472926b4b65f483c4aaff95eb0e2637e656ca19c848b703c6e92a7662a582e2b9fb2f58d21211712a5223ba210fab2515aea84175f96488fb3a31423602539f43ef2de74aee530c4373c87faa9ba47cca398ea8204fafdf088725231f46b84579b253057a57efab16b7affa07a0b17ace9a11a3b3c27baf45a30e5fb9d33fb3cbcfad74b5ff480ea257c87293dd6f5c04443d45354999732f07c454694946f47e0b8a00620ae3f29ff5d28ff1c291f629a4355db088a6da6ab366bcbdfe83ded407060a28adeaffc6006553157a2f83284285881e73e91038e6e5e67aa82d6c7f155748def36946f268e0fafba15b0ea7e9db84ce7406ac7273a7271a2fea46326d54310c883b5496a08f576353a1eb220eaabeddef72f24e67f631d63cd0d98af4c4fad8479f90c5c62ddd74c4d0bfba8c81c93e2cb44e228b40435ff7c57fdf811f4632ace47f9b8cd5aa7d23a58256e262e6714e68a1035f773fe5258890c0961de0b7c4de0fde70ec2ba046a7a3ea73cd5503f9371ba0fb3a8efed51ca94e2ac792a2a64bab07c1aaca48299b6533bd12b29a8abc0c483197a58f150c7e73b611d38ba19d7536ff5901c6ecca1972817e1247abd9328f031d86c22f22f1ac80062ebb4921c6aa36d0fcac5be073653bf85102559a0e5a7182d5f7ff19fd5f1b27394543881d696b933368d97cc561593efab4ab2a9992da8d05682a879cbf8b8b5e6415d7c48dcf8fd77244c7b9c82e77b148eed8696fade16a5a8e7cc1e2ff1528d873ed0142c373dfffc982e7b9d67daec14ef8f9f0e7919d5473894793a124a55246e1bba75af9fe9aaffc740ff87e4400f533e6e48cedf8f51427b9ba4897ba2162432f5722bf1f97aa1ae3fe85aff5f41fd7be172f0a28ab5f3547c35f4ec57ea117e7f43cdafe1596ae43efb04a9ec0a336408f2f2f375d4b5c72bfb12ea7700b31ec66f234894e29f1aa59a8cfbf5f5dba9c98f04ccb851b2d9f3506a54dbf83699774d7ea92c81de37a4014a39f43384fb53baa43f5bf4b8c4d29f1e00fd976724e6f480fe77f4901cfc5c620d639e4cd3b4e6a7f829679eccd3f78f16e0770625a07343724c95e79bad4becb39fea2bec8573a929851ec61f6b3f2e93903cacd5db82cefafb13fa04e84793853d85fe25958a11dfa2a32d5803aabbead36eca4de8acb7259cb12696db2a8e3cc39af8f5b33fd544f94e68df1e38a4651c28b4796a6677c4dbe6597e20243051f1e55c265792a39b159fbf4454d1fd6cb92df4b35ddb6f6bc45d1cd31642f1f933743aca3f4a9a634be0fdf7b9b930fd587f24a6e6dd36d3dce96daa80ded368edccb0b5f4ee3eb3f9384da687df7bb50452fd0531a91ecc8e36329aeb590d9afb7931527cf912a3b973af77ed6fd6bd72734d8ff27107b3a719997ac3e1bf30a6264e4ac47ba399e6257f5e3e45b60aafbba32fe12704d7fe8544360fe7b3788a1139c87bdd4e3a9ca9b2fa40177d5f6db2137e46096db3ae692ceb6e1f63cca3ffd141aed86709fa2eced3e493bdaa11b7b66a44406967b832c392af2f24a6d4a75642784d17ff99096017837707d8dc7607d664b551bfbad9dac2fc23cc492af9c229e651f7fb78d74929417ca118e331adb150d1b38187dfc07be906b9969c4aabf684e7c5315031342550e1d4c0dbb24a31217fea90f492db5f870dbf3daaa6e4fe1689f77def66bcafa5f765ef9f52cb983d17e4535323fb45be2cb6ec062bcfda9f4e4d1122da86c9f31ac7f2b6996921293cfcae605385f5b942835c99587c26a741bafc1144e0000d7f2c312d6d44ca34c8e99bcc1eb55ee9cc2f32bbfd86dc1f7e67d46dd1632654e864a6071f28dd48ed8c9c1499365adbf756047ec34aca15139afa6c5427f951efd65a7ecf51b1bb44042605615e1b6613d89f913a8d46deeb15634e39f0f791c384fee39deb9072e119bc57e530c5984ab9ce9f925d41697bd4c382562bf6570cc56d2e21bb91eb2a8e1630f1fa4dc831b1bb6004107ef38df39707dfdf9fc5b869c6e15f43e6dcb1fc7afeceed21596c2e26b0f32632ed2b1209df48fb52d7776bcdf3dfc531f108478585c9bea7bfa49f94eceb26796cee2618b0ce142069a52436286d74ba9cd2a889a4b1613d1b8d5b7c7b86b78da60a06f215bbfa138d55fcbd8261e39d5f22d2fe968e96e7110325bfa66ed8e26bda98d89266d4f5356514b4be65c4dff8fe509ab6d77758dac8748706988ba4d4076b5237dc2c1d02ffcf965a4e2a62ce28c2bfee78651d481fcefede55f3393e3fbc9157a87d6c969235dda9c8261e3e4350134004b4cd19fc06251d2bc49c1aeeaf40f4371ca2749b01192fa31996f7e7a3da42f6eb4c3d1b5badf52429e89271023a5780f7ba9be78bbf5d9d75920a2191fb4c6a6a13a2b22208f750fc36dfa546916c84de6795b15f8625c6a2fe26972073127d2a34bdfbb24ee8b718e17d3520ff8cf6cdfd9ddd969956edd77112ddd4ae27d1d1f7f43974fd32ce59be847e805d0a0542f47bf1150da0776466eedfc69a384033299a39af504ffa1112a1a2afb0ddd4bb913e41e32db541d488f8552264c868443a52b1e634c2e7a862bfbb397ec37ec7af620b6610e4aba3fdb13b7d5a07f8e143fd51da3a24f4a3165623cc59d591a6350c07402ff2c35f8608c356f0ab45489f20439bca31534cdf381009d336ddc5cc3fc59a3a745ca61395eb4d7d2a5b5393dfca7b60d48b2208429d563d00b96d2009b760914aef24e251578408734770a8e332e98a81a85cef7a0f9f31a4c87f8226c95a9c557a93338e03a7510b2b653f4440ea69552494c2ad2a03bd95a7d85edfee569001bec16fcf46bafc745a5503518b4cba81f6b3b61e49ab3390c99971f783314143e8481c8f627b75bbc9ada95de12ea419161a420aee2d32da2dbf1ad16e3db0db4da114624ef1c77b17ad92b5381d2e480b56a3c3058afc46549ea67f224326696dfbf3fed6e7617febf3b8fe084715a5cfd4100a370229dc09942f1c214e92f2abc5a8c5b9c54558afd82e6a376d723243fba80dc2069456294406d113bafd0ed3ed877fb155927b79fcab039021310a1d2f905e25095c77fa1092c02d74b4e0788162cd134857b1fd49fbb4d2b4104f02ef77242d90f125bfe8494fd0147e48ad59526fdab2976e61a15fe77986ecef676d92bb7ca7fe845b5023d1f1bbbedff5d2349927d1f9cb3ef52329680b51ac013d83347644b5c992a0c4b530320a09aa57936048ba65af6279b3f0ac9e0c478a1e0224d59c34af72e5870cc6a5829634f5fab4ddfaf768c51a21f22bd593be482fdc45603f64bb69abb1bda154af8d408ab550c34edcf058ab134a7b2fc727c7e093cdbc74072ca71a3e0a78d0c14cf8d52bfdaddaac8ef386927fb373e870fb073f84406921048fcaa9e7e9d437fd95a206c40f1621e12d0707ef0df8cef51e3c15ebabdbfb692ca7a0e1dfbb85be113f26cd6c346afc1d4fb405a7a86ffa29491c92442e0ede05f49325ae43a3d24d1115ade305dff99c35422cdb8dad4b4a57cd9cdfa575885f3bbfa944b53b43f1ee8b4ccde33d843c89fd5a4b6dd4b7e3701e514932e8755cf1593c6397f76bc66737e35d6766dc09bebd5f37249dba24b41b6ff3b9cf0c5d8aefb12954c514bedbec00541e542cfc8ad77bca789dc6674a3ae5efc52f0c1a816e86146b1a79af6c11de518e5c83b6ee4babd611811ad5b8eb396c7e2e941bd32ab05dd8588b5dd1e9b346c65fd2084db6c536f45842a9e5d79c9a33e5234c5336b51706f9a2723a85de01c5822fd100487713cf3a5dfbdb845fe75e359316ed5c8477ab39903e31a0dc68890d1dbf39e2668729f468d288bbc6b410e81fac182eba8b98f546f85a78b4a15f87cd944a1e3df9fbbe345904227ca51264435d2279fd34d63a3740a3b2ec7077ccc4ef6f4eab0e478dd5f83f8e4d7302874a2b0847f83bd1011a21ebd7e7a02958d73ebfb4834973c4a37f837100d4061bd76726fbfe2d69d5bea8b149552a25855d5ad05861fed8fd6c6ab297360d746588262d6420da74fce68a4aba45f9c103d18ec836673d1985a8de55289aeeb9b6de02ddd952dfceee14f0b34a1846e23eaddebcca49e1588930a3d6c20ba65ac62e1ff0787fba8da65fb3a8d5c2b62bdbcaafbe8d0a65ccf909fa0d248f3a2be2291c083ab5843cbdf636a27a548b06d0b9da74a18353f498691d0f7a47288543032a2f9590a66df5ad161fa1eb3bb852956905b791e92ad9e1263f12dd9016d29f084bfc0a4af71805952a1c6fa2ac8c16191ff7eb22c55a189d363f6e77fc45d183403b353696981e461b35eab6201b8da24783e594e14ba78c58db99fd95b0ae8fd279a78b5af7a71fd00dcf3a9ac56ade0d989d4af0778454f55ede8ea81619757ca050afed8fee0a2c6d50d0febcc651efe22f165113f57a4bcf42a1a483e1178ad7de3da9741b87593eee3e944d93400abd135b3799f37ca464329f4aad77a62635f508535369f52e52723a9fcaa8771e4d12c949a3679d11aa15eeb7d12b6a9e6c18e9727b966f5a703ae1418ac87e050aa76ae45e46a9e4fc593e38d7945d134aab4e270668f0a94b2d96dd0fb4eebefc605f62aa8f7e830cedd980fbfae57952d1233135f930d20cfcc2cf0def7afafda91e6921be041ef971bfd29087c782b49070c2f0a41eda93caafef416d9538ccf3e587a41319e49e82b46a12b5e451eb49076aaa9c60734036759e3c92863578538565a7f9d08f85921d518a2d021831a9cb03bbe337de4d6645fe6398c1faa29b02c51a3c529bb63e6e9f76885bdfaa639a808463f8eb2a2aa59e27251d48537efdc3d15ae4836b282c11d7d060a8a1c164cbb1aef566af16c97a93370f8dc65f91914ddd679d81581cb08b2bf8772593c00509c0e5630811e54692c995c6e7fd3a825f4732e090146a70c0067e10f4e9e57836a27010210d597ec8d4d7e6e11e4d67cb8626371ea28224302b10a04bc58afcfe448b61ecf21977f8733c51bfe621e94c7f1b7c7b4ecd709dd4d69f1f08b37db6d60ea7cdec8f9436fb3b4fa63fa4cb9963a9c308aad297df4f3329fed63246f778ef9dd3eec9e0adda194fa8613dede76139dfec127df612f30e2b2225800a28429d36352b35011d7146a7859c15f4d3b4f5c43aa3701e2b64fd44bd55ba7c79bf86010f026b67dc14354eb95bd57aadf9c6e35b781fe9f483ba5159acbfa4dee5173496564e9ebd7f994dfcbdf029d1fba36d16d5bdefffc19e0f441f4e8327a49f25edf789531734fd34e51dfd5f917e6207cbb166adebd982089ac72ee9788632069fefd0aa35aeb51dbdc39a61672de9d624d23b9130ebc7442a7b17ef66a262530bea1851f2761b997885828eb492031a3bc688fd3b8c8a4d1284739565a9b7847e1a3b07766d1ccf9fe981d2667af0a24fa68578f00664b77c931a22954ce1eb6def09145e4f797a328b6fd1e1447356bad4aebc3d650abf329d909cb7735883d87727d4394d267b8fde490b392798a269e969b8f7d46237d724dbd27a4ee3eec08739a14ec8ce553627981b56cc59a7135dd6e2a760e706c2bf4a237660f36ee89883383689a04fe65de874dba639867b7a1823ce75f48e6b9482229f574ab3b4b3c4a6b5f5ed1def7526e41f727798e562caac6f67c2b45db3c4139aabccdee533d648f7e13c3fe3b0e5fd5a06344e6908ae15b5053e987177cecd1badb79aeb1eb36762b1ba219f273c75ae4337ca46df5b36257e7fbccdaa7bd15316ed5f64f35edd07fe1feef970b09576a0d90484e8a769615bcbf3ae25348a1c7caa43cb96edd7594294c5509b248261e73b4eb408e9d484e4604ab7cb1d34c1b5a5f3ac9998bf97327147982cfe7c42e737749c22a3fbb434a1c5e23bf9a52f8c9d1a51f376a746c8dfc6da80b5824a1140dfe98176665399644f5e885e9bc597668356ecbfc72bd1597ab128b3980d5936843f2f0ca91625f532d84de5a785a8209630de717267c18c74aaae1529af68f9937aaad664413d58f4c6c7c7cea1436f06516f17f172908da5662674baf90ca05e2fea96340287efb4ad192b7650acd1219d84350f37398d6f7171d08bc2f847b5297efc2a35e8eda55cd39627ed299e86919f439f3c85cfac9d24a4a22fac7b7a3d6bcf8f34a4c7abda9cab681ddc033a68d18e9aac0e0decc368e0398da5b435098319add2d6dfb3d22a26e70b0da089970e59692853eb9106db5d3ef38f5934d4123bacb06e295c25c8e9d82a2821ae01d51ab64e5f929432ba391b7fa379fd465349b69e74900c79942a7b79799b4c3134057fafe95165bef64c31c2b19f6242be5c3162988762a8cae3d66f75bfdefbe561bd7fcdf0c7254fbd407bc7654f4ad79d1f13a4511717d4a38da77e3ac57cb398fc8562a8442476c811b3a92ec964ef6a5ea5ba099d356af7b25c3595f3d4f9922f9563830ac96766bc5b7d613a8cb0339b905e50c44b1ba892658abd2d2c372f583b31fdd25a939bb0071e45735c32c50aaf0667b67f16806e8989296e05b01a0e81b99733ccbd7e10213c8b6fc9532d52383688937ad949609a8a79d2bbd878093e8432f524edb6c08cc2438cdbb211af43dc9bda7324628b1e0cb09364b2f745284792e9c2a6e9097960ede5e635bd725c2004d39fac4b9b5dee265b0c69d92dfe1f6ab304ddd25842d5bc44c984efe06f3aa89e367836c453881b7ae4aa076451854fd1786d3ac23b2b8dcbc26fd9a53645bc4fd5d4fa96506825fe27e00feff4af9034f490da0d9352b37d08fa974f30278d93daa9a4c2fed46c9b77fa653075e597bac0726ef7ce18e44028e4021476e89af6ad636932425c10448f1f1f53df931fe5b830f5b27b2d5b4a0fc9256a9e70829d6b47591fe647156aebc5508f76782dea7da721513750358d29e3679bbd2facebe520b173b53068cb3e7fd9c5527f45f78e47a5cdde2130b4b5a07fa88bb56da824979af648d051a7f5859b2444e10f1295be40c2a32e930e29ea312ede05788ebc51c2aef292eb4630798cc864b54aec793ec7d29e5acbef6a3fe0338024c53946558e64904b5d2efba4dca242d6d86e91446a8ff953dfd6f10aaba621698188b7459294e65d006b27bc2b6a30bd9f4c4cb1d60833b7c3f4cc0de7a0314fe1d084be2ac5a3966ab48e2c3776ea02712a473244f396511644bff7fc88cd85eb7e9dc58f8fcbfe1c2a4bd283e2091d185f111f5a34a6d357683bb5f6d36e1df12a53d42e124ef9b7d5412f13a036adab6fb773b5d42caa1c2fc8456fd91544d1bf8d664ae7f74626f7d63a7816ecb0668346ba7eb57d79a21b39b0e9abedb8edd8fab8044f029d1b4d1c9b83ef848b0b66d37cebb80268537dcee2543bd748cb9b75640a28ba60e7daf1a6dd4cf916f11c5c0f6ad6f5b2cc0d8af21d24eb7a1decfc4d3066bc200749e7b92353f6a66f2e49a695b2e55f9c637966ce5a4a8ceebb36429ebbd12b18c7866e84d1d453782fc79509e1176dd1413b9b8e3a7a003d5df7b4f91eddd3667b38d2736497866e06cd441535eb739e5d2f31fb6d75d1290863f98eee648eabb8179bf64983e62b150efb55b99e7cf484bad828b2f84b26f6e7f271ec2184230e4db93759bef5aedcd20bf7ea173222331e6a2c6984be9252d4994a72d9b00796d094b8097568c257af1c82323ae69ad264618e98e3d491c713d0d758e272723ce46e2325c37ae0be5e775a2351b80e733c85df6ab834b55796a64c33bd5df4c4aa7c46abf21db62adfe189a56c7ebed54cfdd448ebce86fa73f30db89f645a3a20fcec45dc9be55beaf76a8babd46e7a5b5a484ab7add95ffd0a4fbf25ef5f88d934420f59eecabeec742f3b01b1dae60e54b836d8c0486dfbe05788c32eff576c4bfdd2027da6d815d6ed4c7e3e3339a43febec397d44c3f6ad71b1d69cd0379246e01eca5449b677f6b63e7b2c3aeb37992de3ee94a96dab0ae663566fee28a9bd8f8352bb952808a19f05750eeb3bc26fdc1d654e01c16ada954951d48c22418e649c65b67299f215f2d648b02e2fb0cc745e6774b9c33c697c3acc0bd497ec8acfb6f04c5b3f6bc1ee4c0f1af1da70cb781cd670d0523b353b71d93503db0e75e47f77676feab3a5c3df27963b9f4ff0d3cad6e701763461caf92ad4078c2711470b75123e1e013e110f935a46815bba62ad4e454d7f0a2360b99ba35a9ad36236bddbb4cd494d25d78b6f69a5a21a9e5e98ccc7dff9b4c9f0575fd2c426929a6183bf60e5e32fb684fd5b0e768c4e9b3b7359936fc94ff2a68efc6eeccc65e82eeb5c96f54f5397bb6924f4b787792ded7f37da75b41dbac3d4d56e6f8befe4e2890da6a936ebd22aee9af5da816d985f849fe25bf4773e9aec1d4daf1ca51c2a787cb0a3596675754838107f61647f50d81fe7bcb294aaab053b5c82b10715c9d5c8de5848f6448aa267480f65beacc56525eaa5e474c1d87dd20c19a204d5e8a0f681117f07a721715c49cfce1bdb6469a7a17f4daf43a6f79eb62b9249e8be35e4b0fd8775bd77b6b92a9225885fd162c6bbf62bb5012a53457d3bcc5d363d157b6ea142044821267a288a6cd1655dda5e5b343e3b8ab07f49d15f22ca574bb515449aa74e4425fdd27132badc5531f453d1658de54d70d92ba951f915065d806ae726c5e7a3897e9a349881f14710fdd246a478f0c7121e696353e4fc7184bcfdfd773e28f9a0cf87ed8bdf5952b204e66e8ad4648da7a2ff57227a6fc052cf97a28d785c36499b5299baae459883cadc7b9ac5bd24e16c3fed21153c5eb6330a664ff21497ce1954d1dfa5368494b86b99ab95ad354984f7f09ed74dcb709d5ec67b14f1f4a2ef78f49783dd2bee9becabef5fd66948ba8d325ce92f5f4d4ee47d895aa3a632ef217c2a0675251de67702bcf7383fe2987bd8d8e57662ef1af7081b8da8d532bb2bfb06af1509bfcef7e862360fc959f82dad50786b197e7bc48cf5b51473c7f7f38b863ab8a9725c8c083fc773ab8252cea5a945684b39df4f84f0fe4eccdb5d5770d1715cf0f97513923a77acc2bf55e57e73e443bd6c2c3a5a90498dbd29f41ddfb33fe1267a6cf610d988347e1dfb7a663a9e57f4f7247c4ef52ec23bb0ef60f7040dd5ee6b0d6b20fcacdf7bba846d34e1dd8c1a8dbab35bfcf08e7e1392f0eee21b6fe698989d838c02948cd3d7870827e94f3e361b9e1480fe0ad168d1a38fbdb4d4cfa705caba3d881830ec1cb6495a3d24dde916d3bf91f63914c34f924fc4a1a5a2a2562f2d0eef50e6e05358e5af2acc1850f6c5b69e65469143729b73639e5154f35a655e5b70cada545942defc3c26a7e33438a78ecb4b1af119eeb805bcaf789f2b4f2cd46ae507ef1e7e70b4e178cbe936ef6f9919c6de60afdb5e8f32a9759221192fa38cda51ec6e4445829d51f8c93910ea08b1ed87274f084f9846889fb18467d846728ed9235043263963db8867de1b287d05e648c29348c2c8f6bb9c31ddc1f43fe28c59769249bec172c696d1b53237ad5a4230bc317ca2e4fa6fd28c3a626bbd5e88f0d3984cf765bb159f7f8a145f7f8a5c6bbf502be42da82dc4fbd1fcbf6126aba2a885f73b0caa47039e6350adf3b130a8b6a0df67505dd058f64d285b4cf5601854a9893483ea9185415533d58a4175ff2f32a842e8eff0ef776550759c0c7b9e6550d1f55214e0d5dab186ffe65f6650ddef6050ddff7f9941757f41d40b1854f7ffcf32a876b9fe2bfca95b515df853b368fed4dd17f2a7caae7abd883f8535e66833bbaa0ef857f953d6b1c8fdfe15fe141dc3232606a47a317fcabab42c7f8aeea9eaaf3dc79f0a78317fca3a95a2717fc09fa2e38daa7c8e3f15f062fe54e773e733f239fed4c64efed416f41c7fea32e64f59851f66cd9f5af387fca932c7e7f8533a2bfe94d4f67d532acd9fcaaf7f3fc6a1a8fb7f843fc53cf1595ed739fc29a6d77993cb9f2af0c5bbeef82660fe14732f48b0e64fedb0e64f31e3dfb43fe64f31b1f45f8ef953ccfded6556fca98d7f993ff533c39f829a67faf337f6a7532e983f2561f9532d88bec7aca8f5e42e0b479697f0cf7064db42a8e47f9949b5fdd9009a3fc27ccd1ef0b22bfb962f95e5a68f2f50d37a3a8f6652312d30a98349c5983d6826d575999afe6efa1d2b26151d76e0bbdecd05fef43eacb31926d59eae4c2ae6edce449a4945a78afab34c2a26572f7730a9e4ccd7837f894925e9c2a462fa0d19cba4a2733af02d0e938a7966fb7098544cbf25c5bfedb14caa6e7fc0a422582695c38b98549b7deecda3f23b98543f6ff5609954d7ff4526d50f7f814935ed392655d47f8249157494786397ebf262afec309a471540f3a8e6d33caa369647e5c8e1513df8a7785413383caa299807c5acc476606ddf9fde9547b59ce151e5ec627854a7052fe251316dfcc48a47757d91358f8a717ffca73caaf57f8d4775ac0b8f6af69ff0a8fefe9fe051e5d03caad9ff091ed5b017f2a878899847b5333ce6d1af3f5af3a878899d3c2a5ee23fc7a38a38f86ff3a8e8bea6def98f785469233a795403d83eb948ed956dea26dc526cbfde8778e3ec518b7de827ff511e159d3b6db77f994745872725ff3c8f6a13e651e5d6f794a8a9d52fe251e9d35fc8a3cabbd2ae4fb7e6516df3b1f0a82efeb8e777795438362b1ed5ea0e1e95eb531e05bd109560e1516dcdf1cee8f8b5d0eb775854cb318b6a8f76bef1b60dc3a16aeb69c5a14a7e4c73a8be28bd6db3c32ccc1ef66327836a0d694aff3306153d53c30c2ae684bbed94731706d5643cd35d38cec2a07af53906151d1e33a898f08510fe050caaeecf31a8ac6788c91b29372e832a8661505576615031e3d7df680615ad0bea5ffe8c43d439e393af855907cda0baf72f30a83ae329caa57a320caa42016650ad717e11836a338f195ff70e548c8095e4d0fba864dd9e2e0caa37ab0bfce8df6d3c5fc411b14a2f8372c70caaf2119841758f615031f3911fff7ae97d522957864175ef5f62505de8f81ab02809ca4f33a8eefd1506153de7f19771195456f1ad84f27530a8f4e8771854c7690695a3207001ac4e6ea91906152ff1abedde8f180655678cc971304fc10caa3e0c836ac7d74c4dd7767b9e41d5194abe0cf4e32f30a8b6a01731a8accaf31ed4cf1f31a898f9a0e8390655f98b19549d31a3484a66c5a0ca5a8719545a5f86415560db8392d9f7c066864175ef9f665075a614fa2ed5836150dd9b7b795d71fa4c0b83eaca6354982df84b0c2aabf69803cf771706d5bd99c582cd3e5bb79842d6dc7a017feafbcddf322d76eab7df7d369819fa4c863f859f8de7f95356e59961cd9f3af44ff0a7acf423f0dfe14fb1fd1d531f017f953ff58a357fca8ffe5df7d68bf85338f61236f6a2d7ff127f8a7e2e4f5f7f9e3f65a56faf71f9535bd05fe74f59b5ffb87f8e3f65d5bea35ec89f62ca39c5b31ceb8d4680b5a6aa40f17921c2bca6922efc29abf61bfe97f9533f75f2a7ca7d9fe34f95cde45715b0ebbcb3983bd5992acd9fa235d7f38c853f55883af9539db9f119fc62fed4ad28963f351ff3a7987577f871863fb505fd2e7faa0af3a7acf45d61b2b7f0a72ceb6e1feac5fc29abf6965bf3a72ce1cade4df8fa42229547a677c97f6f980361e6547edb3d5883ddef644ee9059839f5eb7dcc42c2ec11d1e3c0dbf83bf3e1e9aea72604e8ea0b42e9b700b6ee33e79dc45c8f1dd9bb2fba2f3ef40d668ce85b1e77bf904ef346e69222f7c5e2d0de5bdda753018d6867477ed06b63efcef3157ab68d9e77b293c1329a007fbabd08c7a8c9def4cda76a6ad363ab5045e385373b7923ccd95c9dae3ee6804b6d334f19ad992aec6e62831edbec2e65cac2fe1e7946f4d82bfd707a6bdca69b847f9626b5006a41d688144502fcbd87d8bd8c39010ca7e0468a50fe793df9a83bd554cfb89663d6083e2b59c89edb6ed09ce231ac02b666c67aa53b9de78fd298a564b260f9e63067eabd0a815eb40c9f8ddb7a1a594e5780d51f9ffab502e1932f2ce7bee213263acf02c1bf582826609f429ea96f5d2b3ee302eef9109ad7624cf163d24bfea6c5e8bea8f7f2c056cc08c0cc8019cdc18fe73c7ae7fe823b91b717d51cd43db8c81f2f425e5943d253a61001b96f48a652628988ffca4944519fdaf2315f27919c7cf6a260badd62f7d0e2f4425e6e5e610e09fd8b3b3ab7a1f8643a9f7a720515a6cb50717a7f58cf913ed1157b3433346c79bdbc729de8b7f64fe9d64b0ab2309814472448b14b82544569e36c117f6c3652d9ce36da2d8ecc6042d624315ca126238410d32126ec2204df456e2d4e5fa7520cdd45f89e74b397a042810451719f120372c5a18b32aa2c6b1bcd3cdf3d9b95924d3ec5dabd2aac4bde5bf139f2389c264f71a405e9cc3875c2cfc262b8b56ad2fb3bdfe77d70ebbd498b772ee62dc1ac86019833c5e8a5d62b57d560b7d862f67979a411efe7e811a5bf2ae427c9da42cad3a4df8bf886e63b48d9dc489fdcdd98d796dfb696da043651fbd0d87aa19a9249f9ae4594930956d515a837b6d9b61051eecd88ea750a51599588da20a67d2b47d7a0711ad021f583d2b85017f44a11fe428a5a7387875d898bd259cd3c9340dc8e6d857e948b09cd5f2d397d4cdff023d63c7c12af1ced582885bcb5b9d079ab16f1952b5d08e56f90b74419c1e6ad970b6158f809a272c5229c4a4b48ef7d94b38c30dca940a68de2764a2a166177fd111724a9573eac4014ea26a00ac5885a07799d4b121a757e397336725bc8831b1af5f5522ae80e6a0b696b69349a6e45b553eebf21d3d23dad296ad347a75a6b8d20378b5bef1a4d31bf81dd4da3c9966c097a963239c8f8afe558a7fedfcb71efe5c18f236f5b9eabc0d619cdcc737530e770d66f83b16eb4cdd4ff88b5a33cd9d0f40b4a19fc2047fa63233fb7c22b0befc44a096ac5d2906bfcd502bfbce5091adf0b3d85c21f72bad918b2f722622a1170689fa17e17f24aaf3df169d900963b8a4e139e52d2c9ac1795f1f1fb40fc959494fc8eef96351a15934d3e8675be8421e431ba9ea3ece54b347c70cad690fd379f69ec9e8ff2e3105a2814249516a68f6e35f45c8788290da5867a5fe28713fa0201b13fdb7eaa2ac074826c4e0a51a5291fdf8332482b6da004ca15b5a84eabbff6943ff1b89b5084f36e87cff6d057fdc457deae43cb1384ea409950383b475f30010d5f8f771e7e19fdb0fa9df56dce63fd9593e7127ab113a2c2ab1c64a3f03901fe1159a2831bd64d3ab8213523c5979892f28670ea5823bedb9f2d99ea683444ad23765f1c56d68f2d77f261c2530fe59692506efc857f60134f2afa8e5fa81d858a45553e4ad96364a8fa09e1bc180a5ad1f2443f5229dcebb3bb34928d21f42053f625a56eda51adca75f761a67ab75409e9482e4a6174a2024964af5605ec30e2bd5b093fa17a6229be93a87b97daabed4b85ea1d91de32c2afcd9998ac0af0886c0b4972d6579244c3eae3ce2db995b99ab48f5cda720db54282821e294eee8a5edcb378b2fb1a1bb2a0a5d2b448faae0d4f5f45f20ca55308c3c1df90f2fd7ba825cfa051f30b57778735040f1948334f49fecc3364f8f3a5d784bc920da33653094f616d982af32ec0e1c76dc7312ae3ee21431384ce955ec371f279ca2613e24987e80d9a7a88e11e4f69730d6279c2d3570b7947371c2ea01aeb11de3779dc66fc7bb3f406c96bcc533655a25499be52c03b5ad098230d11f0127294b678df650dcfa0a985780a78fa99b6bc7e05473bf75d5e2fad06ffebe9723db98d941ffc8294da7ac230ba9ef0de365e54e613bb59b9229818dfed3bd58c0d06f3bbc830fa2932c47d8494ab4fa0f602e5387fc2d03dcae77281a1db4355aab372d4056418de44786fb8b6c130fa0641f81a56bfe343ad682294dd2eaaa423fa2025791829337ef6512e8576d794f8188c1791a1a40419b44f7c085feaf14524fdd20995ac371cae44a30a0c4baf104ab242a5975541c86ad528527a6524df501f8598fddca7adbd94775828957d89cee751734891b45a8634d952a7c7fc1ff2cee60993f323db9c4f6fa082bfb3933625a291788fe8685744e5dc15d8aba9f4bb02fccdb6b4a90939fa53820a8154ab86f235c1fceda8d673df9ccc01e997b50bb43a7fcab6ba4fae3f655fdd47e36f92553f2540cb7ed81f9c79ddb83f735529251323a9568b7e309a7a8bdb715ef02ed34921336449b2fcc85539a737ec88906a6bcd544035ca4d933a5de1cfcf95868c405b8b756a2aad8597abd667349aa9ac1684f75f4d0a3a9a218d9e8dbcb49b8a4dbdc4ed84dff95293084becebba51e8ffe93de99344d44feb719c2d8dee2e0f4a937c97d74f4b6d90f0a44f9af07ed2c4619ed45861c6bb49f72b70dc076e2e904fa3d6dc66eca735b9434e83d57ca9bf33126669fc0c19953e52d287a42657dbe9858ff86e5ae829ce55c2fc0609366d3354d623a5d315783eabd1b45c7a1ffd5c65c86da44f4c40bb2b744c2e56b7e0dda7335aa0cf9608f4898d78f7e98c6a01e6ae2a0fed455e05de9a91fbdc84642bfe424019b40ba54ca6320f23bd1889c76beff8185c6e23437d33626227d441465caf7a5283ced2f5aa97c188f01389705ab8fea1256c45c7a5d183d1c42ff4b26abe7dcafc5cfbb4147f13121dd727ba22530e5976dea8acd5a2dee56c0ed35be8bdab5bd07af88fcf0df128daada6dbac7234c23111ea14ff5387c1ce45dc7edeb82312a7d3e6dc72d8ba07293f6c7d8aed8c9adffacb51db0cfd651eb12346fa3d8c817df557457ce90f8d7ce98d277c436310323c0946d299957c65dc6fc46aa1b4ef48b45a240daa8219473391d27fb3ae3cf7ec6a7d9f51885adf4c18eaab884d2669601f1813a42a8f08bc8a958a403bdc870ca3dea9e946ad158b95e0c7f557aae789658687b50495d22d5e5f50890e643c5d4d9d2605e5eacafd98553eccf474db810dcad287c4d30dfa82d388aa2617ef88d248a4e006bae2951414e894e49490abafec83f4ed00e746bee1612561785845e8839da02e3c77510e2778741a79dd165bd2786b9f893cd1ae515f905d5fadc71a73a11edaf8119f3a51cf930a90c0758ba1200329ab1e22c3b6d5e8529ec1fb3b88b39a3054d5a1634618f95aa5a05386ea27089e7671058230e2f1e46d1fe53ae8ffe169b7f84ff14f31e2bc607365e9aa8b540fbe58595b4b383e38ada3f2cf84e29aa2b2c905fa822ac82b7581747635260551bdf6a215db0f6c3078ff4c3cd9f074f5699da391d29e11e0ef6edc32f12fa990e393a484cef1595228055d1c2f14aa0c21dd09e5badd48d9f77fd0fc1ce56b358432e636a1bc2a24566c1fb5d130ea67e2f88669a5d4e633884e378d9c87d33dad9b6d1c2f7ee443d7466e90d14d206ec5dfb628af68607e4a899b11c42f1e2f16aa94eb8510632d618999981c6154dec6e66454592aed26859cd59a9509ed68762d2e25e5c017504838834e4d47be4cb7e5970f09832603e90b60c6f93d39bd2df882739bf383d5296a4d80f4c94768f6434a448ec1bf02a6fc8dea7b82470985b3f4d59e7c4d36fef66f2d496d59889475109f48142cdd5085a8d745a85ced7aca80edf4a211b8757744b7b90695c2cc69fd99f614f5d9524a7081f7749b32bd12190e3f2494ddaf809e98f9fa1b7d106e9b7fe8f4c17d61340e30e69f84fe71c31544b94ad03f745f14419fb9ee0a32b949dadb82dbde29d1ad574bfc31239ed29d9c62ef4fa59c9c426d104c913e69677aaa297a4d208c0295d0660756077c9a7f12fab38db711e52641a00781ebd5d33e93a8a975b791c9958951afab3713ea25466a768d0db55e2cc2cf8489242f60cd773219bafdacc23d9af2b59f0943469d8ad5ff094933472627e4acc86afbcc243c7199d1857a11a30b7faebd4b8c269b131718cdbff44f69b2a6b45393977f4339319a4c3ca0d69f1941b7700a3986d1a7589d53316871efbd4899f75095bbda50026d9e51a5c29a1cabf32c35e9ceb4625d4e1948f7d66f3689a438f7be98cf8904bdb72a679991458f95e1820e7d335437d231e25116b7a421a3da27c1684a3dd38c677ff83ba31f72938c6ee950a64efdcdaca7f5b7986cf451ae87f056fa9ba29e566aa5bf62467f0d89b01eda463a2bef804b2d94ab07df8672108ba89ec20152dd6db374ac2b4af123de342c2d8531b8db20a9735fa40f31f3a5054e48bf6205f22e005d3e0ea362f6e3fef82483c7fd34ea943bfa154f90a39f74d4693355b8a7df5becf908585b53023c0c413fc00c4ba51cf52bcc202a7df2f3a41b609c9827a24bab4c3fcc945678c52756176d54c63421cab17b9fcebc6c2da59f1489b87f972765f3429ee3a9ce6784ea21e6c5ea4c3d841734fe3ba25302898073ce6daee5ea61fbe04959cb3c292642f82d76c56e29816dae52ac73f3489887439f2b823e37edac7b5248a00c9f430a73f3f555f0147923989f475412b9a9d4a6bb282837c55f724ae35fe24cf811013b229242da9ca4c1246168fc0a1d776a585d992b4cfbc8b50566aa9e04731640e799e6bc45f8bd86d206f47ef49b043caf3ebd73e8933e6e90226596a5260209a79c58dd2bf884107a3fc6b256c2d34de467c64fc0f03cea5485088ff7f9db943d4f2243f035149463e8730e51bea324c592d7548519ae88d63675b5006bbed33665df934859790329375c00ed008d1b550612e6734157d0b063ca982c64ead5bd39e0d80c4b7a8d6e308f5f05fda4df3365cf2c9412087d669e2ba373ef42afe88f77f180b8c510b733c45d77a123cefc634c7f67d09df5b9b9da942fd97058b3226f7e31d683f3a5f8e96834621b8f63259a7179ab8a356ae87d7e928eca30e339cef0829b067cf28ce4a73623ee5140cf0e61795a77bd34c5ff7c29731fababc5ae01e761ee21417acd6a74e8b0a9b7a4dd6443ded2d70f44230fa5046efa167aba8dd0d3d94a10f443927af330e8ed4c8557da4d1249bbbe3e043dc9101e02bdd8b8f0a75a83492cdc1ff1a569fbc2cfa67dcd9c7fa1dfe0841c23cbd34ead959275e607b036863917da5d77368f507ff52b7e732025e1c979bb9a379a44c9527593f995ed5f9dd20beb0815f4a98705fa2a19b1e35689ec6cde5958d94c92fd90b73c316580dfc091db9505f56876ae63941bfe855c54ed63a8aea6df826f2a1f4fd6f8ccbf24f4f728c26f2554fe71726734bbd4b213c6d8f3f88b16fdcc463e355388705f804fb4a0cfcea05b2df45a7e64a1b68e3f5ebbd7a710d620301f945d41b08e0a4e72965e1111cbf36ed2dfbae2bc6852f5eb1ff31bf37686137ed773f197afb02253a7f89eed480dd7023e03049f0082cf5891217c1e4efdc7d1393807f9dfe447e9b5649977813ec417c5f684beb3c8c394a28ef8c9929be44b6e90dbafd4d4c57a744caddbee463af1f1ba59d9f336c265af28936cbf0bbdb393d9002b4bc2b7d138f8a410d6b4a3f93757e338bd65fac3cee8873c8fa8b5a2d89e49328f88a4e024277d908888ce9b246bcbfd215798dadeb321f754eea6e37145cec8b2d3022e8b47548a3a4976c8f8db00dc923b16e2771d42be34b3d92cad6ae4b7e5ea0b4622652249e8832af9d7735b5c70e90ccd32226580becf4844658b9d977f24edd6cd67dc367cb60cf56385e4d3e3b9f8ac6d9efd85b61069b79a8f09f5fac93a3f7c46329577d82517fae0c32e9beaf48db042f1a3d276a37c92b8884f6f96ecd3a94d054f5aa3ab52d4d26e8f3e5e3f595aa0c56f4990b01b7d9e10ffa1735bf00d67fc5d3236e7a651ce0f514a051ebd08ffa4106942fbc7c3d26638b5e47de43c3ff7ad5c2a47dc33aec805a52653af97d9eac9b256eacdf66ed8b794ff1d9445c93fa1c2bd849bb01ba2ce4709dcc423117e7b21d96e28e84628ab1fd2f3f61ff20c30cf399f4704367c232d90217d4222ccf2379dd7f951b9876d72fd3493a98cc336509e044b791c49cb793744607e51e3ace2cc7d98c12f305c7582391225fa0db9912305ca8db548b9f02aa11c8360862c26fc8c7a59029da2f4ea05580d92c45a21a4aa4e99ac6f4c44a635c2fb2609f950dae482ce1e33d45712443df6fb7763ee455873c8a07dae7af2f17ba6b5627df505fe5ae1f55c8dfae6a66bb2f379b82e615e510ae38e71e23f3cc85ca366f2d81bb5a5d83e65f2f2632647d19d1de4a163267b515d2dcc93741760a652508724e798f61b5c24519b0aebda975cd3df9985661fb29c4783cf114df1871caaf5e2357c7ca2a8573a354788c60b927d18bd46fb5079d24c37a10059de135b87f94e88c350ef08117e27ed952ebadd193274172a278a88a291cb07d48a1e1c48ef77374bd31912095f949a7c074e8d6614333c899e38e57ba153c2f787dbd3e74fbe8c325ad95f7b9eb68550924634dccf2da8d247a8ab5b864f4f24fc6a4bd9b2b129d5bcb05cc99b714ad627615b8779f4c2303e055dc3148ca6bf9f89dd48e1d05e69de99bd7f61ce6685de1a6209b5c7b1402edd1b115b3efd176afc4e9f39a30f875718f72ee58f11e3f987bf52c8272c3960c24ed29b7a46b4d2fe0c7b174fd2dc3476cd2712bc289f65395df3b9254ce1d088b644281ce17fc0bcf29f02143de16e9ac2a911fd344de10cf7cb1532f8bfcafdeed76113ee6e993fa1f6eb08f7075f2f716ffc3ac1bd2d351912418b279acd9f01ae01867b9bcde9806f017b7ccce6fd80d3800f01f75566f32c90aac5d1514b23c2e511b1b131b11e3668aeafdfa420f57cb9dfd2f88858b9ffe2b0b885f26931e11188bd2ceeb40b7618278f5e1a1d2f4f088b8d873b08149bb02cfe85fe16c7c42c43366c3c094b17862d0d5f0c295b05c25a1a131b210f4f58b26c9cdc33cede06bd1e961017211fbad273e8b0518b57be2cf79bfe7a87099c516c44d45ccfa1c3c3e70ff28c1b2c9f68e5f3f75cece92c44fe9ef320cfc8c12fcbffd019c7e03204d1f5cd9593df7ab1bd457e37fd8fdd77bdf6c7ee455718b98f95475859c6caef587995957758f98895a892916256bab052ceca311cb30fc73c9995d359b98c952b5999c9ca35ac3cc23197b1f20c2bef70cc8f38e6668e195575358b396607d6ecc2ca411cf3508e790cc7ecc3314fe698a773ccb338e6508e7921c7bcacaa6b7daee4989339e64c8ed9e17b363e560ee2988772cc6338661f8e7932c73c9d639ec53187b272212b9339e64c56ae61e5e71cf3115696b1f22ac75cc331dfe1981f71cccd1c3362f55ccc4a39c73c88631eca318fe1987d38e6c91cf3748e7916c71cca312f64e532566672cc6b38e68d1c7364fc503996fed3e5f1114b96c5c486c54647c461fb6172c67d382b95ac1cc1ca91ac1cc5cad18c8ceb882f2e6c0574c7d0f145c7418f1c47d726b8b3f18675f80b8b8d4a5812b1343eeed5d888f884d8a5f215618b13222cfec32cfe873fe7dfda6364189bbf30367f616cfec2d8fc8559f2d711cfdb2fce1f1b4f1c1b4f1c1b4f1c1b4f9c259e31ac1ccbca614359398ca98f3196746676d6abfcf5b0c58b23626977365c3c1b2e9e09f77e446c0c76981c161bfe4a62742ce4105ba1d830b91cdbcf60aa282c3c3c36220ef21cb78cb17f3b3e8c7a4fbe2c861efdc01cc5daab17c72c085bdce910cfdacf5c181b11166e653f94b57f5166e10275a0dd411d681937f4d5c86538dd2e95f86a646cd89208ab6ca038365c577f0885b1e9f9272ca5e2a36396feb116807f369eb0e1bf17ae6b005007c6ff08568e64e528568e66cb31fc45f98b43716cf838367c1c1b3e8e0d1f67093f86956359396c282b8731f5c5c613cfc613cfc613cfc6b36f0c3bceb2b29995f2b1ecf3cacac9ac3c3d8e7d7e5973fba83f960ba099dfa3db83a997c8b084c5f1f2c888786aa19579714c58b8b57b5c3c4c9b687334a840142850f4d2b8f8d804baca69fb25d17161ec54af3332b882e5833c17270c86c9d638b9951f9c00d42e15b32202ab7458bcdc73e898c52b87c817472c1d87e7452f638d1e475bbe0cadc0d88587c5878d63e75f717444e33c8785bf1c09b1c5e33bb7b94397b013404e5abf970fba60ff5e465e947a97f43b2b0ffd777ef5dff955d7f9ce7fe7575dcdffb7cfafa09318af1cb6c4cfaa3b7a33215e1e13295f12b124267695bd0dd38dbc3dfbedd75553a732fe954b42acfcc7ad8a7b376225ac89293c1e87cb17ac92d3ab5acfc5e1f2c4e8f885782d8997a15dfaa317a41bb4342e61190ceef1b88f5a158763c3718c93870d9de88957b861c358391c4b0f4e8cffe225628480632db436f078ff66225617f9cf78e6ff659f2b4733edf9841d57a7b3e6dbe3197994959b2730f229eb6f086b064d41048007e0f785fa78ed732404880024400c90006c00b6003bf0630fe806e80e70406bf88e207b407829c89e209d00ce702f03b8f45d835c01bd006e0077406f401f405f801ce001e8075000fa033c0103000301830083015e8097002f035e010c01bc0a180a1806180e50024600460246014603c600c602c601c60326002602bc01af017c20af2ac8e72490af83f405e907d21fcaaa064c060400a600de004c054c0304829f3701d3016f0166403dbc0d7226b805411cc1801030cf02cc06cc01f35cc03cc07cc03b60f72e201410065800a000e18008708f0444011602a2c16e11e03dc062c012c052400c6019b82f07c402e200f160970058014804ac84165e05781ff001e0434012e023874de86340322005900a4803a443180d2003a085f832013a4016201b9003c805ac06e401f05f3e60edd04d480f5807f7eb011b0005808d804d10d7664021600b602b601b603ba008b003b013fc7d02d805d80dd803d80bf6fb009f02f603fe06f80cf077c001c0ff003e077f5f000e020c802f0187c0fe30c0083802f80a7014500cf81a700c500228059401ca01c70127002701a700a70115803380b38073806f00df02ce032e002e02be035c025c067c0fb8d2174f8dd6c0f4620daa065c05fc003574adef26f423dcff04b80eb8016df033c81af07f13700b6002dc06d402ea00f5803b80bb807b805f00f7010f000f01bf021e011a00ff00fc06780c68043c0134019a012d80a780678056401ba01d60ee4b770033d8aee46d46f49fce9a67e27f57e69bcdb7008f00ed00bb77cce65e8041803180c9805980858095804c4011e0734009e05bc035c01dc01380e85db3d9193000300ae00f08064402560032003e8039805840166023600fe04bc071c077801ac043402bc026d46c76018c02a48799cdeb00fe703f00100c88047c04580dd802d80f3802380db8021801fe6f837c0c10c07d4f802f6026a03f201c100f380eb80ab8076805382f8034001300c1808580f701ab01fb016580ab80fb00310579063c0608c2cde63e20c70166002201a9808d80cf0127018f001700556cb8db009f08b33910f00e6001600d600fe021a05b24e40550c2ba79c37d157b8fdd4bc07c1950071812056503b9089001780ab001bb5e518cdf87567162390fecb30047001bc1ee6064a77b1dd88d8a60b08005b643c883d5a37eac54d012af58de4d8c0e87e981f744f948f98001722bab0913e56318dfb05859b600e6161e13e541aa1933df7d7be69bd3df1dfe3bf6c3de1d69190f3dfa052c85057774b87c59586c74fcaa7eacfd8a776171b5e2dd0509911e1303836042c35eafe2d45f9d1e1bb328828235fef0a1c3c6befa5ec4d2f0d855f111ef26c6c4be17b72c8c8ae8b07a252e3e6c6978d8e298a5701bfedeab8ba317bc1a1e1b0d8bb6b857f14f03432836de4130b3f10c1fccbcdca7f345bf09c1cb3b58a825ca3dfafdc7d28f8c8d88888d8f897b3506e6634be36357e14cf883e58c996fbe6d29e70be65d9d59b49a4fad9c1916f7de7f385ff110651c5b33092b67c62c9b1e1b1d831b07d7937c71745cfcd480b767be1bf0f6bb7ed3a6cf9c3d48be6ca56fc4e2b05511e1383353c17d6ec2cae971b101e1f3e583e58351bf95a0ab803eacf45fd5691ec002db6133cf0a4300a11f30580198c722f0fd4e33be9fc7bacfb1c2ba0fb14ec345f0787cb804ec25e45ca23fb9c87ff312ff2f5f2fa740df9fcbc0dbea1ee36172e7bd2fb8cd63dd83412e60ef3f0379f02fc03a5e2e4ea633d23d15fab174c66c01d76f95555c07b2baba796533725176a7dde7d97f9c3637fd399cf47f4a61ec9c53193fad6cfa8b21ed2f01ed0087d4cef4b1bf6556693e62efb1fdf3b37dcbd5fa5a5733dfa7abb9fdb580407ffa2e7a69242d035581b45c1ac6bc271b3a6cb872c4c851a3c78c0d5b408547445a4276daab26bdeeebe7df61cf4a39e71acab9e481618172ecef9580a591f817d35572265c5723026fb4f4f30ca725d48cb9d5dc6c7e64ae31979993cdc88cda512b6a468f500d2ab3ac55201ff2d68b277628524cafdcd87ee1f219c1f53d576dc253bfb97cef1f577a7806dc7e65d48837df3e70e13dcfd71a373ef9febd59c41eedb1b2906bbb53f7ec3c347c58fd15c13f4cc3bf1fbedcfdb2e2c7e10f03d6cc19ebbd53fcf6b3f620c7fca7635ddfa73c5f1f56f6d98425b76efc38ffbb27860f3f2bbafd75f887fedf5d0ff2967bf55f6050576f54bcb6fbeed8dd03ef7dbccfebb5b9b6ff9892b2fb81cb8da8a3ebc6fafdbdcf1717d7bdb6bcd4f0a96ed5c2922153e7de9c76b264ddfabab3af4d9b38f1c3a3a35eb23fded67a212cdd31f1dbbfdd7e2da87cdfae401f69d889ef5f8a70bd354b66bcd4f09a4df527cb2676f9d7fcdaf96fb9d7334edbb357b299b65f11b176ca94e4d79ca69edd5c20cfe2cd083de999307cd18c0b9b875f18346defedf9134fcd1f9fbd6ded9cc5332e4ccf6e1834d13e426697909291b0e2ece469915b9a674dff7ae6bc886f4fe777db73a0c8d5f4897f1f8df15efb46b735c5d7736fb46c7df6cb47fbdf7a7d246f34f1c5a8c5ca98d33fe7dc4d3eab3eaadfa76a9efeccf3bd8ae5ef954ea8dc73d11cd077e5f1ef3b7452fe68eba1652fedfbfba9add15df554507ad66df5088eee22f9d725ae5c3be4b3c5ee393b7446f8bc5d11ef793babeb75564e7ff3ed8059208774719dfefaea2f60ae738c4191d53dc6742bf31eb82f61cd47409e64efbb1d84f9e35f8075bc5cf43fc4c88fc0df975f32660bb87ec758c5e55cdcd56d2d6bbe6c65eff2f51fa7cd4dff28277d6fd66e259bee3bacbc7214fa43482712107bb0337decefaa55fab3d8f4b13dddd87fd29f747a18842fae77b93cd3a1eb25e8b8580b157d112fb818f749f4c57bc1855de57fa44cff87ae5ddf9acd185f03b6b1083fd769c6f7db58f72d56d87301d6638f3afa4ee66516c1bed32298774c5e42f6fda8a0b38fc5575923fb5ef909fb7eb3a9abfbffd72f8295dd94e7ff36a2c792bafb3784c8f67f3547ffbdfe7bfdf7faeff5dfebff4fd71d765cb5c8668e143777952e1c398823c770e4648e9cc5910b397225476672e4468edcc7914738f20c475ee5c83b1cd9cc91e296aed285230771e4188e9ccc91b338722147aee4c84c8edcc891fb38f208479ee1c8ab1c7987239b3952fcb4ab74e1c8411c3986232773e42c8e5cc8912b393293233772e43e8e3cc2916738f22a47dee1c8668e143feb2a5d387210478ee1c8c91c398b231772e44a8ecce4c88d1cb98f238f70e4198ebcca917738b29923c5ad5da50b470ee2c8311c3999236771e4428e5cc991991cb99123f771e4118e3cc3915739f20e473673a4b8adab74e1c8411c3986232773e42c8e5cc8912b393293233772e43e8e3cc2916738f22a47dee1c8668e14b777952e1c398823c770e4648e9cc5910b397225476672e4468edcc7914738f20c475ee5c83b1cd9cc91627357e9c2918338720c474ee6c8591cb9902357726426476ee4c87d1c798423cf70e4558eb45c66f31fbdeffde7afff747cffa9ebf5ffcbddd77ec9b46b4961d7f675f995d5ab078cacf9e93f23cd5d2efc5e43f5f6eb01017f90c33f73672ecb7ba3ce9899f7463ecb6c3aca95fcc05bf6a711fdc5ebff019e470e55'
+ ISP_PROG = binascii.unhexlify(ISP_PROG)
+ ISP_PROG = zlib.decompress(ISP_PROG)
+
+ def printProgressBar (iteration, total, prefix = '', suffix = '', filename = '', decimals = 1, length = 100, fill = '='):
+ """
+ Call in a loop to create terminal progress bar
+ @params:
+ iteration - Required : current iteration (Int)
+ total - Required : total iterations (Int)
+ prefix - Optional : prefix string (Str)
+ suffix - Optional : suffix string (Str)
+ decimals - Optional : positive number of decimals in percent complete (Int)
+ length - Optional : character length of bar (Int)
+ fill - Optional : bar fill character (Str)
+ """
+ percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
+ filledLength = int(length * iteration // total)
+ bar = fill * filledLength + '-' * (length - filledLength)
+ KFlash.log('\r%s |%s| %s%% %s' % (prefix, bar, percent, suffix), end = '\r')
+ # Print New Line on Complete
+ if iteration == total:
+ KFlash.log()
+ if callback:
+ fileTypeStr = filename
+ if prefix == "Downloading ISP:":
+ fileTypeStr = "ISP"
+ elif prefix == "Programming BIN:" and fileTypeStr == "":
+ fileTypeStr = "BIN"
+ callback(fileTypeStr, iteration, total, suffix)
+
+ def slip_reader(port):
+ partial_packet = None
+ in_escape = False
+
+ while True:
+ waiting = port.inWaiting()
+ read_bytes = port.read(1 if waiting == 0 else waiting)
+ if read_bytes == b'':
+ raise_exception( Exception("Timed out waiting for packet %s" % ("header" if partial_packet is None else "content")) )
+ for b in read_bytes:
+
+ if type(b) is int:
+ b = bytes([b]) # python 2/3 compat
+
+ if partial_packet is None: # waiting for packet header
+ if b == b'\xc0':
+ partial_packet = b""
+ else:
+ raise_exception( Exception('Invalid head of packet (%r)' % b) )
+ elif in_escape: # part-way through escape sequence
+ in_escape = False
+ if b == b'\xdc':
+ partial_packet += b'\xc0'
+ elif b == b'\xdd':
+ partial_packet += b'\xdb'
+ else:
+ raise_exception( Exception('Invalid SLIP escape (%r%r)' % (b'\xdb', b)) )
+ elif b == b'\xdb': # start of escape sequence
+ in_escape = True
+ elif b == b'\xc0': # end of packet
+ yield partial_packet
+ partial_packet = None
+ else: # normal byte in packet
+ partial_packet += b
+
+
+ class ISPResponse:
+ class ISPOperation(Enum):
+ ISP_ECHO = 0xC1
+ ISP_NOP = 0xC2
+ ISP_MEMORY_WRITE = 0xC3
+ ISP_MEMORY_READ = 0xC4
+ ISP_MEMORY_BOOT = 0xC5
+ ISP_DEBUG_INFO = 0xD1
+ ISP_CHANGE_BAUDRATE = 0xc6
+
+ class ErrorCode(Enum):
+ ISP_RET_DEFAULT = 0
+ ISP_RET_OK = 0xE0
+ ISP_RET_BAD_DATA_LEN = 0xE1
+ ISP_RET_BAD_DATA_CHECKSUM = 0xE2
+ ISP_RET_INVALID_COMMAND = 0xE3
+
+ @staticmethod
+ def parse(data):
+ # type: (bytes) -> (int, int, str)
+ op = 0
+ reason = 0
+ text = ''
+
+ if (sys.version_info > (3, 0)):
+ op = int(data[0])
+ reason = int(data[1])
+ else:
+ op = ord(data[0])
+ reason = ord(data[1])
+
+ try:
+ if ISPResponse.ISPOperation(op) == ISPResponse.ISPOperation.ISP_DEBUG_INFO:
+ text = data[2:].decode()
+ except ValueError:
+ KFlash.log('Warning: recv unknown op', op)
+
+ return (op, reason, text)
+
+
+ class FlashModeResponse:
+ class Operation(Enum):
+ ISP_DEBUG_INFO = 0xD1
+ ISP_NOP = 0xD2
+ ISP_FLASH_ERASE = 0xD3
+ ISP_FLASH_WRITE = 0xD4
+ ISP_REBOOT = 0xD5
+ ISP_UARTHS_BAUDRATE_SET = 0xD6
+ FLASHMODE_FLASH_INIT = 0xD7
+
+ class ErrorCode(Enum):
+ ISP_RET_DEFAULT = 0
+ ISP_RET_OK = 0xE0
+ ISP_RET_BAD_DATA_LEN = 0xE1
+ ISP_RET_BAD_DATA_CHECKSUM = 0xE2
+ ISP_RET_INVALID_COMMAND = 0xE3
+ ISP_RET_BAD_INITIALIZATION = 0xE4
+
+ @staticmethod
+ def parse(data):
+ # type: (bytes) -> (int, int, str)
+ op = 0
+ reason = 0
+ text = ''
+
+ if (sys.version_info > (3, 0)):
+ op = int(data[0])
+ reason = int(data[1])
+ else:
+ op = ord(data[0])
+ reason = ord(data[1])
+
+ if FlashModeResponse.Operation(op) == FlashModeResponse.Operation.ISP_DEBUG_INFO:
+ text = data[2:].decode()
+
+ return (op, reason, text)
+
+
+ def chunks(l, n):
+ """Yield successive n-sized chunks from l."""
+ for i in range(0, len(l), n):
+ yield l[i:i + n]
+
+ class TerminalSize:
+ @staticmethod
+ def getTerminalSize():
+ import platform
+ current_os = platform.system()
+ tuple_xy=None
+ if current_os == 'Windows':
+ tuple_xy = TerminalSize._getTerminalSize_windows()
+ if tuple_xy is None:
+ tuple_xy = TerminalSize._getTerminalSize_tput()
+ # needed for window's python in cygwin's xterm!
+ if current_os == 'Linux' or current_os == 'Darwin' or current_os.startswith('CYGWIN'):
+ tuple_xy = TerminalSize._getTerminalSize_linux()
+ if tuple_xy is None:
+ # Use default value
+ tuple_xy = (80, 25) # default value
+ return tuple_xy
+
+ @staticmethod
+ def _getTerminalSize_windows():
+ res=None
+ try:
+ from ctypes import windll, create_string_buffer
+
+ # stdin handle is -10
+ # stdout handle is -11
+ # stderr handle is -12
+
+ h = windll.kernel32.GetStdHandle(-12)
+ csbi = create_string_buffer(22)
+ res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
+ except:
+ return None
+ if res:
+ import struct
+ (bufx, bufy, curx, cury, wattr,
+ left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
+ sizex = right - left + 1
+ sizey = bottom - top + 1
+ return sizex, sizey
+ else:
+ return None
+
+ @staticmethod
+ def _getTerminalSize_tput():
+ # get terminal width
+ # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window
+ try:
+ import subprocess
+ proc=subprocess.Popen(["tput", "cols"],stdin=subprocess.PIPE,stdout=subprocess.PIPE)
+ output=proc.communicate(input=None)
+ cols=int(output[0])
+ proc=subprocess.Popen(["tput", "lines"],stdin=subprocess.PIPE,stdout=subprocess.PIPE)
+ output=proc.communicate(input=None)
+ rows=int(output[0])
+ return (cols,rows)
+ except:
+ return None
+
+ @staticmethod
+ def _getTerminalSize_linux():
+ def ioctl_GWINSZ(fd):
+ try:
+ import fcntl, termios, struct, os
+ cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,'1234'))
+ except:
+ return None
+ return cr
+ cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
+ if not cr:
+ try:
+ fd = os.open(os.ctermid(), os.O_RDONLY)
+ cr = ioctl_GWINSZ(fd)
+ os.close(fd)
+ except:
+ pass
+ if not cr:
+ try:
+ cr = (os.environ['LINES'], os.environ['COLUMNS'])
+ except:
+ return None
+ return int(cr[1]), int(cr[0])
+
+ @staticmethod
+ def get_terminal_size(fallback=(100, 24), terminal = False):
+ try:
+ columns, rows = TerminalSize.getTerminalSize()
+ if not terminal:
+ if not terminal_auto_size:
+ columns, rows = terminal_size
+ except:
+ columns, rows = fallback
+
+ return columns, rows
+
+ class MAIXLoader:
+ def raise_exception(self, exception):
+ raise_exception(exception)
+
+ def change_baudrate(self, baudrate):
+ KFlash.log(INFO_MSG,"Selected Baudrate: ", baudrate, BASH_TIPS['DEFAULT'])
+ out = struct.pack('= 4500000:
+ # OPENEC super baudrate
+ KFlash.log(INFO_MSG, "Enable OPENEC super baudrate!!!", BASH_TIPS['DEFAULT'])
+ if baudrate == 4500000:
+ self._port.baudrate = 300
+ if baudrate == 6000000:
+ self._port.baudrate = 250
+ if baudrate == 7500000:
+ self._port.baudrate = 350
+
+ def change_baudrate_stage0(self, baudrate):
+ # Dangerous, here are dinosaur infested!!!!!
+ # Don't touch this code unless you know what you are doing
+ # Stage0 baudrate is fixed
+ # Contributor: [@rgwan](https://github.com/rgwan)
+ # rgwan
+ baudrate = 1500000
+ if args.Board == "goE" or args.Board == "trainer":
+ KFlash.log(INFO_MSG,"Selected Stage0 Baudrate: ", baudrate, BASH_TIPS['DEFAULT'])
+ # This is for openec, contained ft2232, goE and trainer
+ KFlash.log(INFO_MSG,"FT2232 mode", BASH_TIPS['DEFAULT'])
+ baudrate_stage0 = int(baudrate * 38.6 / 38)
+ out = struct.pack(' 3:
+ err = (ERROR_MSG,'Fast mode failed, please use slow mode by add parameter ' + BASH_TIPS['GREEN'] + '--Slow', BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ self.raise_exception( Exception(err) )
+ try:
+ self.greeting()
+ break
+ except TimeoutError:
+ pass
+ elif args.Board == "dan" or args.Board == "bit" or args.Board == "kd233":
+ KFlash.log(INFO_MSG,"CH340 mode", BASH_TIPS['DEFAULT'])
+ # This is for CH340, contained dan, bit and kd233
+ baudrate_stage0 = int(baudrate * 38.4 / 38)
+ # CH340 can not use this method, test failed, take risks at your own risk
+ else:
+ # This is for unknown board
+ KFlash.log(WARN_MSG,"Unknown mode", BASH_TIPS['DEFAULT'])
+
+ def __init__(self, port='/dev/ttyUSB1', baudrate=115200):
+ # configure the serial connections (the parameters differs on the device you are connecting to)
+ self._port = serial.Serial(
+ port=port,
+ baudrate=baudrate,
+ parity=serial.PARITY_NONE,
+ stopbits=serial.STOPBITS_ONE,
+ bytesize=serial.EIGHTBITS,
+ timeout=0.1
+ )
+ KFlash.log(INFO_MSG, "Default baudrate is", baudrate, ", later it may be changed to the value you set.", BASH_TIPS['DEFAULT'])
+
+ self._port.isOpen()
+ self._slip_reader = slip_reader(self._port)
+ self._kill_process = False
+
+ """ Read a SLIP packet from the serial port """
+
+ def read(self):
+ return next(self._slip_reader)
+
+ """ Write bytes to the serial port while performing SLIP escaping """
+
+ def write(self, packet):
+ buf = b'\xc0' \
+ + (packet.replace(b'\xdb', b'\xdb\xdd').replace(b'\xc0', b'\xdb\xdc')) \
+ + b'\xc0'
+ #KFlash.log('[WRITE]', binascii.hexlify(buf))
+ return self._port.write(buf)
+
+ def read_loop(self):
+ #out = b''
+ # while self._port.inWaiting() > 0:
+ # out += self._port.read(1)
+
+ # KFlash.log(out)
+ while 1:
+ sys.stdout.write('[RECV] raw data: ')
+ sys.stdout.write(binascii.hexlify(self._port.read(1)).decode())
+ sys.stdout.flush()
+
+ def recv_one_return(self):
+ timeout_init = time.time()
+ data = b''
+ # find start boarder
+ #sys.stdout.write('[RECV one return] raw data: ')
+ while 1:
+ if time.time() - timeout_init > ISP_RECEIVE_TIMEOUT:
+ raise TimeoutError
+ c = self._port.read(1)
+ #sys.stdout.write(binascii.hexlify(c).decode())
+ sys.stdout.flush()
+ if c == b'\xc0':
+ break
+
+ in_escape = False
+ while 1:
+ if time.time() - timeout_init > ISP_RECEIVE_TIMEOUT:
+ self.raise_exception( TimeoutError )
+ c = self._port.read(1)
+ #sys.stdout.write(binascii.hexlify(c).decode())
+ sys.stdout.flush()
+ if c == b'\xc0':
+ break
+
+ elif in_escape: # part-way through escape sequence
+ in_escape = False
+ if c == b'\xdc':
+ data += b'\xc0'
+ elif c == b'\xdd':
+ data += b'\xdb'
+ else:
+ self.raise_exception( Exception('Invalid SLIP escape (%r%r)' % (b'\xdb', c)) )
+ elif c == b'\xdb': # start of escape sequence
+ in_escape = True
+
+ data += c
+
+ #sys.stdout.write('\n')
+ return data
+
+ # kd233 or open-ec or new cmsis-dap
+ def reset_to_isp_kd233(self):
+ self._port.setDTR (False)
+ self._port.setRTS (False)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to LOW, IO16 to HIGH --')
+ # Pull reset down and keep 10ms
+ self._port.setDTR (True)
+ self._port.setRTS (False)
+ time.sleep(0.1)
+ #KFlash.log('-- IO16 to LOW, RESET to HIGH --')
+ # Pull IO16 to low and release reset
+ self._port.setRTS (True)
+ self._port.setDTR (False)
+ time.sleep(0.1)
+ def reset_to_boot_kd233(self):
+ self._port.setDTR (False)
+ self._port.setRTS (False)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to LOW --')
+ # Pull reset down and keep 10ms
+ self._port.setDTR (True)
+ self._port.setRTS (False)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to HIGH, BOOT --')
+ # Pull IO16 to low and release reset
+ self._port.setRTS (False)
+ self._port.setDTR (False)
+ time.sleep(0.1)
+
+ #dan dock
+ def reset_to_isp_dan(self):
+ self._port.setDTR (False)
+ self._port.setRTS (False)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to LOW, IO16 to HIGH --')
+ # Pull reset down and keep 10ms
+ self._port.setDTR (False)
+ self._port.setRTS (True)
+ time.sleep(0.1)
+ #KFlash.log('-- IO16 to LOW, RESET to HIGH --')
+ # Pull IO16 to low and release reset
+ self._port.setRTS (False)
+ self._port.setDTR (True)
+ time.sleep(0.1)
+ def reset_to_boot_dan(self):
+ self._port.setDTR (False)
+ self._port.setRTS (False)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to LOW --')
+ # Pull reset down and keep 10ms
+ self._port.setDTR (False)
+ self._port.setRTS (True)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to HIGH, BOOT --')
+ # Pull IO16 to low and release reset
+ self._port.setRTS (False)
+ self._port.setDTR (False)
+ time.sleep(0.1)
+
+ # maix goD for old cmsis-dap firmware
+ def reset_to_isp_goD(self):
+ self._port.setDTR (True) ## output 0
+ self._port.setRTS (True)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to LOW --')
+ # Pull reset down and keep 10ms
+ self._port.setRTS (False)
+ self._port.setDTR (True)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to HIGH, BOOT --')
+ # Pull IO16 to low and release reset
+ self._port.setRTS (False)
+ self._port.setDTR (True)
+ time.sleep(0.1)
+ def reset_to_boot_goD(self):
+ self._port.setDTR (False)
+ self._port.setRTS (False)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to LOW --')
+ # Pull reset down and keep 10ms
+ self._port.setRTS (False)
+ self._port.setDTR (True)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to HIGH, BOOT --')
+ # Pull IO16 to low and release reset
+ self._port.setRTS (True)
+ self._port.setDTR (True)
+ time.sleep(0.1)
+
+ # maix goE for openec or new cmsis-dap firmware
+ def reset_to_boot_maixgo(self):
+ self._port.setDTR (False)
+ self._port.setRTS (False)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to LOW --')
+ # Pull reset down and keep 10ms
+ self._port.setRTS (False)
+ self._port.setDTR (True)
+ time.sleep(0.1)
+ #KFlash.log('-- RESET to HIGH, BOOT --')
+ # Pull IO16 to low and release reset
+ self._port.setRTS (False)
+ self._port.setDTR (False)
+ time.sleep(0.1)
+
+ def greeting(self):
+ self._port.write(b'\xc0\xc2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0')
+ op, reason, text = ISPResponse.parse(self.recv_one_return())
+
+ #KFlash.log('MAIX return op:', ISPResponse.ISPOperation(op).name, 'reason:', ISPResponse.ErrorCode(reason).name)
+
+
+ def flash_greeting(self):
+ retry_count = 0
+ while 1:
+ self.checkKillExit()
+ self._port.write(b'\xc0\xd2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0')
+ retry_count = retry_count + 1
+ try:
+ op, reason, text = FlashModeResponse.parse(self.recv_one_return())
+ except IndexError:
+ if retry_count > MAX_RETRY_TIMES:
+ err = (ERROR_MSG,"Failed to Connect to K210's Stub",BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ self.raise_exception( Exception(err) )
+ KFlash.log(WARN_MSG,"Index Error, retrying...",BASH_TIPS['DEFAULT'])
+ time.sleep(0.1)
+ continue
+ except TimeoutError:
+ if retry_count > MAX_RETRY_TIMES:
+ err = (ERROR_MSG,"Failed to Connect to K210's Stub",BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ self.raise_exception( Exception(err) )
+ KFlash.log(WARN_MSG,"Timeout Error, retrying...",BASH_TIPS['DEFAULT'])
+ time.sleep(0.1)
+ continue
+ except:
+ if retry_count > MAX_RETRY_TIMES:
+ err = (ERROR_MSG,"Failed to Connect to K210's Stub",BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ self.raise_exception( Exception(err) )
+ KFlash.log(WARN_MSG,"Unexcepted Error, retrying...",BASH_TIPS['DEFAULT'])
+ time.sleep(0.1)
+ continue
+ # KFlash.log('MAIX return op:', FlashModeResponse.Operation(op).name, 'reason:',
+ # FlashModeResponse.ErrorCode(reason).name)
+ if FlashModeResponse.Operation(op) == FlashModeResponse.Operation.ISP_NOP and FlashModeResponse.ErrorCode(reason) == FlashModeResponse.ErrorCode.ISP_RET_OK:
+ KFlash.log(INFO_MSG,"Boot to Flashmode Successfully",BASH_TIPS['DEFAULT'])
+ self._port.flushInput()
+ self._port.flushOutput()
+ break
+ else:
+ if retry_count > MAX_RETRY_TIMES:
+ err = (ERROR_MSG,"Failed to Connect to K210's Stub",BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ self.raise_exception( Exception(err) )
+ KFlash.log(WARN_MSG,"Unexcepted Return recevied, retrying...",BASH_TIPS['DEFAULT'])
+ time.sleep(0.1)
+ continue
+
+ def boot(self, address=0x80000000):
+ KFlash.log(INFO_MSG,"Booting From " + hex(address),BASH_TIPS['DEFAULT'])
+
+ out = struct.pack(' MAX_RETRY_TIMES:
+ err = (ERROR_MSG,"Failed to initialize flash",BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ self.raise_exception( Exception(err) )
+ KFlash.log(WARN_MSG,"Index Error, retrying...",BASH_TIPS['DEFAULT'])
+ time.sleep(0.1)
+ continue
+ except TimeoutError:
+ if retry_count > MAX_RETRY_TIMES:
+ err = (ERROR_MSG,"Failed to initialize flash",BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ self.raise_exception( Exception(err) )
+ KFlash.log(WARN_MSG,"Timeout Error, retrying...",BASH_TIPS['DEFAULT'])
+ time.sleep(0.1)
+ continue
+ except:
+ if retry_count > MAX_RETRY_TIMES:
+ err = (ERROR_MSG,"Failed to initialize flash",BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ self.raise_exception( Exception(err) )
+ KFlash.log(WARN_MSG,"Unexcepted Error, retrying...",BASH_TIPS['DEFAULT'])
+ time.sleep(0.1)
+ continue
+ # KFlash.log('MAIX return op:', FlashModeResponse.Operation(op).name, 'reason:',
+ # FlashModeResponse.ErrorCode(reason).name)
+ if FlashModeResponse.Operation(op) == FlashModeResponse.Operation.FLASHMODE_FLASH_INIT and FlashModeResponse.ErrorCode(reason) == FlashModeResponse.ErrorCode.ISP_RET_OK:
+ KFlash.log(INFO_MSG,"Initialization flash Successfully",BASH_TIPS['DEFAULT'])
+ break
+ else:
+ if retry_count > MAX_RETRY_TIMES:
+ err = (ERROR_MSG,"Failed to initialize flash",BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ self.raise_exception( Exception(err) )
+ KFlash.log(WARN_MSG,"Unexcepted Return recevied, retrying...",BASH_TIPS['DEFAULT'])
+ time.sleep(0.1)
+ continue
+
+ def flash_dataframe(self, data, address=0x80000000):
+ DATAFRAME_SIZE = 1024
+ data_chunks = chunks(data, DATAFRAME_SIZE)
+ #KFlash.log('[DEBUG] flash dataframe | data length:', len(data))
+ total_chunk = math.ceil(len(data)/DATAFRAME_SIZE)
+
+ time_start = time.time()
+ for n, chunk in enumerate(data_chunks):
+ self.checkKillExit()
+ while 1:
+ self.checkKillExit()
+ #KFlash.log('[INFO] sending chunk', i, '@address', hex(address), 'chunklen', len(chunk))
+ out = struct.pack(' 1):
+ speed = str(int((n + 1) * DATAFRAME_SIZE / 1024.0 / time_delta)) + 'kiB/s'
+ printProgressBar(n+1, total_chunk, prefix = 'Downloading ISP:', suffix = speed, length = columns - 35)
+
+ def dump_to_flash(self, data, address=0):
+ '''
+ typedef struct __attribute__((packed)) {
+ uint8_t op;
+ int32_t checksum; /* All the fields below are involved in the calculation of checksum */
+ uint32_t address;
+ uint32_t data_len;
+ uint8_t data_buf[1024];
+ } isp_request_t;
+ '''
+
+ DATAFRAME_SIZE = ISP_FLASH_DATA_FRAME_SIZE
+ data_chunks = chunks(data, DATAFRAME_SIZE)
+ #KFlash.log('[DEBUG] flash dataframe | data length:', len(data))
+
+
+
+ for n, chunk in enumerate(data_chunks):
+ #KFlash.log('[INFO] sending chunk', i, '@address', hex(address))
+ out = struct.pack(' MAX_RETRY_TIMES:
+ err = (ERROR_MSG,"Error Count Exceeded, Stop Trying",BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ self.raise_exception( Exception(err) )
+ continue
+ break
+ address += len(chunk)
+
+
+
+ def flash_erase(self):
+ #KFlash.log('[DEBUG] erasing spi flash.')
+ self._port.write(b'\xc0\xd3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0')
+ op, reason, text = FlashModeResponse.parse(self.recv_one_return())
+ #KFlash.log('MAIX return op:', FlashModeResponse.Operation(op).name, 'reason:',
+ # FlashModeResponse.ErrorCode(reason).name)
+
+ def install_flash_bootloader(self, data):
+ # Download flash bootloader
+ self.flash_dataframe(data, address=0x80000000)
+
+ def load_elf_to_sram(self, f):
+ try:
+ from elftools.elf.elffile import ELFFile
+ from elftools.elf.descriptions import describe_p_type
+ except ImportError:
+ err = (ERROR_MSG,'pyelftools must be installed, run '+BASH_TIPS['GREEN']+'`' + ('pip', 'pip3')[sys.version_info > (3, 0)] + ' install pyelftools`',BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ self.raise_exception( Exception(err) )
+
+ elffile = ELFFile(f)
+ if elffile['e_entry'] != 0x80000000:
+ KFlash.log(WARN_MSG,"ELF entry is 0x%x instead of 0x80000000" % (elffile['e_entry']), BASH_TIPS['DEFAULT'])
+
+ for segment in elffile.iter_segments():
+ t = describe_p_type(segment['p_type'])
+ KFlash.log(INFO_MSG, ("Program Header: Size: %d, Virtual Address: 0x%x, Type: %s" % (segment['p_filesz'], segment['p_vaddr'], t)), BASH_TIPS['DEFAULT'])
+ if not (segment['p_vaddr'] & 0x80000000):
+ continue
+ if segment['p_filesz']==0 or segment['p_vaddr']==0:
+ KFlash.log("Skipped")
+ continue
+ self.flash_dataframe(segment.data(), segment['p_vaddr'])
+
+ def flash_firmware(self, firmware_bin, aes_key = None, address_offset = 0, sha256Prefix = True, filename = ""):
+ # type: (bytes, bytes, int, bool) -> None
+ # Don't remove above code!
+
+ #KFlash.log('[DEBUG] flash_firmware DEBUG: aeskey=', aes_key)
+
+ if sha256Prefix == True:
+ # Add header to the firmware
+ # Format: SHA256(after)(32bytes) + AES_CIPHER_FLAG (1byte) + firmware_size(4bytes) + firmware_data
+ aes_cipher_flag = b'\x01' if aes_key else b'\x00'
+
+ # Encryption
+ if aes_key:
+ enc = AES_128_CBC(aes_key, iv=b'\x00'*16).encrypt
+ padded = firmware_bin + b'\x00'*15 # zero pad
+ firmware_bin = b''.join([enc(padded[i*16:i*16+16]) for i in range(len(padded)//16)])
+
+ firmware_len = len(firmware_bin)
+
+ data = aes_cipher_flag + struct.pack(' 1):
+ speed = str(int((n + 1) * ISP_FLASH_DATA_FRAME_SIZE / 1024.0 / time_delta)) + 'kiB/s'
+ printProgressBar(n+1, total_chunk, prefix = 'Programming BIN:', filename=filename, suffix = speed, length = columns - 35)
+
+ def kill(self):
+ self._kill_process = True
+
+ def checkKillExit(self):
+ if self._kill_process:
+ self._port.close()
+ self._kill_process = False
+ raise Exception("Cancel")
+
+ def open_terminal(reset):
+ control_signal = '0' if reset else '1'
+ control_signal_b = not reset
+ import serial.tools.miniterm
+ # For using the terminal with MaixPy the 'filter' option must be set to 'direct'
+ # because some control characters are emited
+ sys.argv = [sys.argv[0], _port, '115200', '--dtr='+control_signal, '--rts='+control_signal, '--filter=direct', '--eol=LF']
+ serial.tools.miniterm.main(default_port=_port, default_baudrate=115200, default_dtr=control_signal_b, default_rts=control_signal_b)
+ sys.exit(0)
+
+ boards_choices = ["kd233", "dan", "bit", "bit_mic", "goE", "goD", "maixduino", "trainer"]
+ if terminal:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-p", "--port", help="COM Port", default="DEFAULT")
+ parser.add_argument("-f", "--flash", help="SPI Flash type, 0 for SPI3, 1 for SPI0", default=1)
+ parser.add_argument("-b", "--baudrate", type=int, help="UART baudrate for uploading firmware", default=115200)
+ parser.add_argument("-l", "--bootloader", help="Bootloader bin path", required=False, default=None)
+ parser.add_argument("-k", "--key", help="AES key in hex, if you need encrypt your firmware.", required=False, default=None)
+ parser.add_argument("-v", "--version", help="Print version.", action='version', version='0.8.3')
+ parser.add_argument("--verbose", help="Increase output verbosity", default=False, action="store_true")
+ parser.add_argument("-t", "--terminal", help="Start a terminal after finish (Python miniterm)", default=False, action="store_true")
+ parser.add_argument("-n", "--noansi", help="Do not use ANSI colors, recommended in Windows CMD", default=False, action="store_true")
+ parser.add_argument("-s", "--sram", help="Download firmware to SRAM and boot", default=False, action="store_true")
+ parser.add_argument("-B", "--Board",required=False, type=str, help="Select dev board", choices=boards_choices)
+ parser.add_argument("-S", "--Slow",required=False, help="Slow download mode", default=False, action="store_true")
+ parser.add_argument("firmware", help="firmware bin path")
+ args = parser.parse_args()
+ else:
+ args = argparse.Namespace()
+ setattr(args, "port", "DEFAULT")
+ setattr(args, "flash", 1)
+ setattr(args, "baudrate", 115200)
+ setattr(args, "bootloader", None)
+ setattr(args, "key", None)
+ setattr(args, "verbose", False)
+ setattr(args, "terminal", False)
+ setattr(args, "noansi", False)
+ setattr(args, "sram", False)
+ setattr(args, "Board", None)
+ setattr(args, "Slow", False)
+
+ # udpate args for none terminal call
+ if not terminal:
+ args.port = dev
+ args.baudrate = baudrate
+ args.noansi = noansi
+ args.sram = sram
+ args.Board = board
+ args.firmware = file
+
+ if args.Board == "maixduino" or args.Board == "bit_mic":
+ args.Board = "goE"
+
+ if (args.noansi == True):
+ BASH_TIPS = dict(NORMAL='',BOLD='',DIM='',UNDERLINE='',
+ DEFAULT='', RED='', YELLOW='', GREEN='',
+ BG_DEFAULT='', BG_WHITE='')
+ ERROR_MSG = BASH_TIPS['RED']+BASH_TIPS['BOLD']+'[ERROR]'+BASH_TIPS['NORMAL']
+ WARN_MSG = BASH_TIPS['YELLOW']+BASH_TIPS['BOLD']+'[WARN]'+BASH_TIPS['NORMAL']
+ INFO_MSG = BASH_TIPS['GREEN']+BASH_TIPS['BOLD']+'[INFO]'+BASH_TIPS['NORMAL']
+ KFlash.log(INFO_MSG,'ANSI colors not used',BASH_TIPS['DEFAULT'])
+
+ manually_set_the_board = False
+ if args.Board:
+ manually_set_the_board = True
+
+ if args.port == "DEFAULT":
+ if args.Board == "goE":
+ list_port_info = list(serial.tools.list_ports.grep("0403")) #Take the second one
+ if len(list_port_info) == 0:
+ err = (ERROR_MSG,"No valid COM Port found in Auto Detect, Check Your Connection or Specify One by"+BASH_TIPS['GREEN']+'`--port/-p`',BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ raise_exception( Exception(err) )
+ list_port_info.sort()
+ if len(list_port_info) == 1:
+ _port = list_port_info[0].device
+ elif len(list_port_info) > 1:
+ _port = list_port_info[1].device
+ KFlash.log(INFO_MSG,"COM Port Auto Detected, Selected ", _port, BASH_TIPS['DEFAULT'])
+ elif args.Board == "trainer":
+ list_port_info = list(serial.tools.list_ports.grep("0403")) #Take the first one
+ if(len(list_port_info)==0):
+ err = (ERROR_MSG,"No valid COM Port found in Auto Detect, Check Your Connection or Specify One by"+BASH_TIPS['GREEN']+'`--port/-p`',BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ raise_exception( Exception(err) )
+ list_port_info.sort()
+ _port = list_port_info[0].device
+ KFlash.log(INFO_MSG,"COM Port Auto Detected, Selected ", _port, BASH_TIPS['DEFAULT'])
+ else:
+ try:
+ list_port_info = next(serial.tools.list_ports.grep(VID_LIST_FOR_AUTO_LOOKUP)) #Take the first one within the list
+ _port = list_port_info.device
+ KFlash.log(INFO_MSG,"COM Port Auto Detected, Selected ", _port, BASH_TIPS['DEFAULT'])
+ except StopIteration:
+ err = (ERROR_MSG,"No valid COM Port found in Auto Detect, Check Your Connection or Specify One by"+BASH_TIPS['GREEN']+'`--port/-p`',BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ raise_exception( Exception(err) )
+ else:
+ _port = args.port
+ KFlash.log(INFO_MSG,"COM Port Selected Manually: ", _port, BASH_TIPS['DEFAULT'])
+
+ self.loader = MAIXLoader(port=_port, baudrate=115200)
+ file_format = ProgramFileFormat.FMT_BINARY
+
+ # 0. Check firmware
+ try:
+ firmware_bin = open(args.firmware, 'rb')
+ except OSError:
+ err = (ERROR_MSG,'Unable to find the firmware at ', args.firmware, BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ raise_exception( Exception(err) )
+
+ with open(args.firmware, 'rb') as f:
+ file_header = f.read(4)
+ #if file_header.startswith(bytes([0x50, 0x4B])):
+ if file_header.startswith(b'\x50\x4B'):
+ if ".kfpkg" != os.path.splitext(args.firmware)[1]:
+ KFlash.log(INFO_MSG, 'Find a zip file, but not with ext .kfpkg:', args.firmware, BASH_TIPS['DEFAULT'])
+ else:
+ file_format = ProgramFileFormat.FMT_KFPKG
+
+ #if file_header.startswith(bytes([0x7F, 0x45, 0x4C, 0x46])):
+ if file_header.startswith(b'\x7f\x45\x4c\x46'):
+ file_format = ProgramFileFormat.FMT_ELF
+ if args.sram:
+ KFlash.log(INFO_MSG, 'Find an ELF file:', args.firmware, BASH_TIPS['DEFAULT'])
+ else:
+ err = (ERROR_MSG, 'This is an ELF file and cannot be programmed to flash directly:', args.firmware, BASH_TIPS['DEFAULT'] , '\r\nPlease retry:', args.firmware + '.bin', BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ raise_exception( Exception(err) )
+
+ # 1. Greeting.
+ KFlash.log(INFO_MSG,"Trying to Enter the ISP Mode...",BASH_TIPS['DEFAULT'])
+
+ retry_count = 0
+
+ while 1:
+ self.checkKillExit()
+ try:
+ retry_count = retry_count + 1
+ if retry_count > 15:
+ err = (ERROR_MSG,"No valid Kendryte K210 found in Auto Detect, Check Your Connection or Specify One by"+BASH_TIPS['GREEN']+'`-p '+('/dev/ttyUSB0', 'COM3')[sys.platform == 'win32']+'`',BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ raise_exception( Exception(err) )
+ if args.Board == "dan" or args.Board == "bit" or args.Board == "trainer":
+ try:
+ KFlash.log('.', end='')
+ self.loader.reset_to_isp_dan()
+ self.loader.greeting()
+ break
+ except TimeoutError:
+ pass
+ elif args.Board == "kd233":
+ try:
+ KFlash.log('_', end='')
+ self.loader.reset_to_isp_kd233()
+ self.loader.greeting()
+ break
+ except TimeoutError:
+ pass
+ elif args.Board == "goE":
+ try:
+ KFlash.log('*', end='')
+ self.loader.reset_to_isp_kd233()
+ self.loader.greeting()
+ break
+ except TimeoutError:
+ pass
+ elif args.Board == "goD":
+ try:
+ KFlash.log('#', end='')
+ self.loader.reset_to_isp_goD()
+ self.loader.greeting()
+ break
+ except TimeoutError:
+ pass
+ else:
+ try:
+ KFlash.log('.', end='')
+ self.loader.reset_to_isp_dan()
+ self.loader.greeting()
+ args.Board = "dan"
+ KFlash.log()
+ KFlash.log(INFO_MSG,"Automatically detected dan/bit/trainer",BASH_TIPS['DEFAULT'])
+ break
+ except TimeoutError:
+ pass
+ try:
+ KFlash.log('_', end='')
+ self.loader.reset_to_isp_kd233()
+ self.loader.greeting()
+ args.Board = "kd233"
+ KFlash.log()
+ KFlash.log(INFO_MSG,"Automatically detected goE/kd233",BASH_TIPS['DEFAULT'])
+ break
+ except TimeoutError:
+ pass
+ try:
+ KFlash.log('.', end='')
+ self.loader.reset_to_isp_goD()
+ self.loader.greeting()
+ args.Board = "goD"
+ KFlash.log()
+ KFlash.log(INFO_MSG,"Automatically detected goD",BASH_TIPS['DEFAULT'])
+ break
+ except TimeoutError:
+ pass
+ try:
+ # Magic, just repeat, don't remove, it may unstable, don't know why.
+ KFlash.log('_', end='')
+ self.loader.reset_to_isp_kd233()
+ self.loader.greeting()
+ args.Board = "kd233"
+ KFlash.log()
+ KFlash.log(INFO_MSG,"Automatically detected goE/kd233",BASH_TIPS['DEFAULT'])
+ break
+ except TimeoutError:
+ pass
+ except Exception as e:
+ KFlash.log()
+ raise_exception( Exception("Greeting fail, check serial port ("+str(e)+")" ) )
+
+ # Don't remove this line
+ # Dangerous, here are dinosaur infested!!!!!
+ ISP_RECEIVE_TIMEOUT = 5
+
+ KFlash.log()
+ KFlash.log(INFO_MSG,"Greeting Message Detected, Start Downloading ISP",BASH_TIPS['DEFAULT'])
+
+ if manually_set_the_board and (not args.Slow):
+ if (args.baudrate >= 1500000) or args.sram:
+ self.loader.change_baudrate_stage0(args.baudrate)
+
+ # 2. download bootloader and firmware
+ if args.sram:
+ if file_format == ProgramFileFormat.FMT_KFPKG:
+ err = (ERROR_MSG, "Unable to load kfpkg to SRAM")
+ err = tuple2str(err)
+ raise_exception( Exception(err) )
+ elif file_format == ProgramFileFormat.FMT_ELF:
+ self.loader.load_elf_to_sram(firmware_bin)
+ else:
+ self.loader.install_flash_bootloader(firmware_bin.read())
+ else:
+ # install bootloader at 0x80000000
+ isp_loader = open(args.bootloader, 'rb').read() if args.bootloader else ISP_PROG
+ self.loader.install_flash_bootloader(isp_loader)
+
+ # Boot the code from SRAM
+ self.loader.boot()
+
+ if args.sram:
+ # Dangerous, here are dinosaur infested!!!!!
+ # Don't touch this code unless you know what you are doing
+ self.loader._port.baudrate = args.baudrate
+ KFlash.log(INFO_MSG,"Boot user code from SRAM", BASH_TIPS['DEFAULT'])
+ if(args.terminal == True):
+ open_terminal(False)
+ msg = "Burn SRAM OK"
+ raise_exception( Exception(msg) )
+
+ # Dangerous, here are dinosaur infested!!!!!
+ # Don't touch this code unless you know what you are doing
+ self.loader._port.baudrate = 115200
+
+ KFlash.log(INFO_MSG,"Wait For 0.1 second for ISP to Boot", BASH_TIPS['DEFAULT'])
+
+ time.sleep(0.1)
+
+ self.loader.flash_greeting()
+
+ if args.baudrate != 115200:
+ self.loader.change_baudrate(args.baudrate)
+ KFlash.log(INFO_MSG,"Baudrate changed, greeting with ISP again ... ", BASH_TIPS['DEFAULT'])
+ self.loader.flash_greeting()
+
+ self.loader.init_flash(args.flash)
+
+ if file_format == ProgramFileFormat.FMT_KFPKG:
+ KFlash.log(INFO_MSG,"Extracting KFPKG ... ", BASH_TIPS['DEFAULT'])
+ firmware_bin.close()
+ with backports.tempfile.TemporaryDirectory() as tmpdir:
+ try:
+ with zipfile.ZipFile(args.firmware) as zf:
+ zf.extractall(tmpdir)
+ except zipfile.error:
+ err = (ERROR_MSG,'Unable to Decompress the kfpkg, your file might be corrupted.',BASH_TIPS['DEFAULT'])
+ err = tuple2str(err)
+ raise_exception( Exception(err) )
+
+ fFlashList = open(os.path.join(tmpdir, 'flash-list.json'), "r")
+ sFlashList = re.sub(r'"address": (.*),', r'"address": "\1",', fFlashList.read()) #Pack the Hex Number in json into str
+ fFlashList.close()
+ jsonFlashList = json.loads(sFlashList)
+ for lBinFiles in jsonFlashList['files']:
+ self.checkKillExit()
+ KFlash.log(INFO_MSG,"Writing",lBinFiles['bin'],"into","0x%08x"%int(lBinFiles['address'], 0),BASH_TIPS['DEFAULT'])
+ with open(os.path.join(tmpdir, lBinFiles["bin"]), "rb") as firmware_bin:
+ self.loader.flash_firmware(firmware_bin.read(), None, int(lBinFiles['address'], 0), lBinFiles['sha256Prefix'], filename=lBinFiles['bin'])
+ else:
+ if args.key:
+ aes_key = binascii.a2b_hex(args.key)
+ if len(aes_key) != 16:
+ raise_exception( ValueError('AES key must by 16 bytes') )
+
+ self.loader.flash_firmware(firmware_bin.read(), aes_key=aes_key)
+ else:
+ self.loader.flash_firmware(firmware_bin.read())
+
+ # 3. boot
+ if args.Board == "dan" or args.Board == "bit" or args.Board == "trainer":
+ self.loader.reset_to_boot_dan()
+ elif args.Board == "kd233":
+ self.loader.reset_to_boot_kd233()
+ elif args.Board == "goE":
+ self.loader.reset_to_boot_maixgo()
+ elif args.Board == "goD":
+ self.loader.reset_to_boot_goD()
+ else:
+ KFlash.log(WARN_MSG,"Board unknown !! please press reset to boot!!")
+
+ KFlash.log(INFO_MSG,"Rebooting...", BASH_TIPS['DEFAULT'])
+ try:
+ self.loader._port.close()
+ except Exception:
+ pass
+
+ if(args.terminal == True):
+ open_terminal(True)
+
+ def kill(self):
+ if self.loader:
+ self.loader.kill()
+ self.killProcess = True
+
+ def checkKillExit(self):
+ if self.killProcess:
+ if self.loader:
+ self.loader._port.close()
+ raise Exception("Cancel")
+
+
+def main():
+ kflash = KFlash()
+ try:
+ kflash.process()
+ except Exception as e:
+ if str(e) == "Burn SRAM OK":
+ sys.exit(0)
+ kflash.log(str(e))
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()
diff --git a/mixly/tools/python/serial/__init__.py b/mixly/tools/python/serial/__init__.py
new file mode 100644
index 00000000..caa4de1f
--- /dev/null
+++ b/mixly/tools/python/serial/__init__.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+#
+# This is a wrapper module for different platform implementations
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2001-2020 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import absolute_import
+
+import sys
+import importlib
+
+from serial.serialutil import *
+#~ SerialBase, SerialException, to_bytes, iterbytes
+
+__version__ = '3.5'
+
+VERSION = __version__
+
+# pylint: disable=wrong-import-position
+if sys.platform == 'cli':
+ from serial.serialcli import Serial
+else:
+ import os
+ # chose an implementation, depending on os
+ if os.name == 'nt': # sys.platform == 'win32':
+ from serial.serialwin32 import Serial
+ elif os.name == 'posix':
+ from serial.serialposix import Serial, PosixPollSerial, VTIMESerial # noqa
+ elif os.name == 'java':
+ from serial.serialjava import Serial
+ else:
+ raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
+
+
+protocol_handler_packages = [
+ 'serial.urlhandler',
+]
+
+
+def serial_for_url(url, *args, **kwargs):
+ """\
+ Get an instance of the Serial class, depending on port/url. The port is not
+ opened when the keyword parameter 'do_not_open' is true, by default it
+ is. All other parameters are directly passed to the __init__ method when
+ the port is instantiated.
+
+ The list of package names that is searched for protocol handlers is kept in
+ ``protocol_handler_packages``.
+
+ e.g. we want to support a URL ``foobar://``. A module
+ ``my_handlers.protocol_foobar`` is provided by the user. Then
+ ``protocol_handler_packages.append("my_handlers")`` would extend the search
+ path so that ``serial_for_url("foobar://"))`` would work.
+ """
+ # check and remove extra parameter to not confuse the Serial class
+ do_open = not kwargs.pop('do_not_open', False)
+ # the default is to use the native implementation
+ klass = Serial
+ try:
+ url_lowercase = url.lower()
+ except AttributeError:
+ # it's not a string, use default
+ pass
+ else:
+ # if it is an URL, try to import the handler module from the list of possible packages
+ if '://' in url_lowercase:
+ protocol = url_lowercase.split('://', 1)[0]
+ module_name = '.protocol_{}'.format(protocol)
+ for package_name in protocol_handler_packages:
+ try:
+ importlib.import_module(package_name)
+ handler_module = importlib.import_module(module_name, package_name)
+ except ImportError:
+ continue
+ else:
+ if hasattr(handler_module, 'serial_class_for_url'):
+ url, klass = handler_module.serial_class_for_url(url)
+ else:
+ klass = handler_module.Serial
+ break
+ else:
+ raise ValueError('invalid URL, protocol {!r} not known'.format(protocol))
+ # instantiate and open when desired
+ instance = klass(None, *args, **kwargs)
+ instance.port = url
+ if do_open:
+ instance.open()
+ return instance
diff --git a/mixly/tools/python/serial/__main__.py b/mixly/tools/python/serial/__main__.py
new file mode 100644
index 00000000..bd0a2e63
--- /dev/null
+++ b/mixly/tools/python/serial/__main__.py
@@ -0,0 +1,3 @@
+from .tools import miniterm
+
+miniterm.main()
diff --git a/mixly/tools/python/serial/rfc2217.py b/mixly/tools/python/serial/rfc2217.py
new file mode 100644
index 00000000..2ae188ed
--- /dev/null
+++ b/mixly/tools/python/serial/rfc2217.py
@@ -0,0 +1,1351 @@
+#! python
+#
+# This module implements a RFC2217 compatible client. RF2217 descibes a
+# protocol to access serial ports over TCP/IP and allows setting the baud rate,
+# modem control lines etc.
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2001-2015 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+# TODO:
+# - setting control line -> answer is not checked (had problems with one of the
+# severs). consider implementing a compatibility mode flag to make check
+# conditional
+# - write timeout not implemented at all
+
+# ###########################################################################
+# observations and issues with servers
+# ===========================================================================
+# sredird V2.2.1
+# - http://www.ibiblio.org/pub/Linux/system/serial/ sredird-2.2.2.tar.gz
+# - does not acknowledge SET_CONTROL (RTS/DTR) correctly, always responding
+# [105 1] instead of the actual value.
+# - SET_BAUDRATE answer contains 4 extra null bytes -> probably for larger
+# numbers than 2**32?
+# - To get the signature [COM_PORT_OPTION 0] has to be sent.
+# - run a server: while true; do nc -l -p 7000 -c "sredird debug /dev/ttyUSB0 /var/lock/sredir"; done
+# ===========================================================================
+# telnetcpcd (untested)
+# - http://ftp.wayne.edu/kermit/sredird/telnetcpcd-1.09.tar.gz
+# - To get the signature [COM_PORT_OPTION] w/o data has to be sent.
+# ===========================================================================
+# ser2net
+# - does not negotiate BINARY or COM_PORT_OPTION for his side but at least
+# acknowledges that the client activates these options
+# - The configuration may be that the server prints a banner. As this client
+# implementation does a flushInput on connect, this banner is hidden from
+# the user application.
+# - NOTIFY_MODEMSTATE: the poll interval of the server seems to be one
+# second.
+# - To get the signature [COM_PORT_OPTION 0] has to be sent.
+# - run a server: run ser2net daemon, in /etc/ser2net.conf:
+# 2000:telnet:0:/dev/ttyS0:9600 remctl banner
+# ###########################################################################
+
+# How to identify ports? pySerial might want to support other protocols in the
+# future, so lets use an URL scheme.
+# for RFC2217 compliant servers we will use this:
+# rfc2217://:[?option[&option...]]
+#
+# options:
+# - "logging" set log level print diagnostic messages (e.g. "logging=debug")
+# - "ign_set_control": do not look at the answers to SET_CONTROL
+# - "poll_modem": issue NOTIFY_MODEMSTATE requests when CTS/DTR/RI/CD is read.
+# Without this option it expects that the server sends notifications
+# automatically on change (which most servers do and is according to the
+# RFC).
+# the order of the options is not relevant
+
+from __future__ import absolute_import
+
+import logging
+import socket
+import struct
+import threading
+import time
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+try:
+ import Queue
+except ImportError:
+ import queue as Queue
+
+import serial
+from serial.serialutil import SerialBase, SerialException, to_bytes, \
+ iterbytes, PortNotOpenError, Timeout
+
+# port string is expected to be something like this:
+# rfc2217://host:port
+# host may be an IP or including domain, whatever.
+# port is 0...65535
+
+# map log level names to constants. used in from_url()
+LOGGER_LEVELS = {
+ 'debug': logging.DEBUG,
+ 'info': logging.INFO,
+ 'warning': logging.WARNING,
+ 'error': logging.ERROR,
+}
+
+
+# telnet protocol characters
+SE = b'\xf0' # Subnegotiation End
+NOP = b'\xf1' # No Operation
+DM = b'\xf2' # Data Mark
+BRK = b'\xf3' # Break
+IP = b'\xf4' # Interrupt process
+AO = b'\xf5' # Abort output
+AYT = b'\xf6' # Are You There
+EC = b'\xf7' # Erase Character
+EL = b'\xf8' # Erase Line
+GA = b'\xf9' # Go Ahead
+SB = b'\xfa' # Subnegotiation Begin
+WILL = b'\xfb'
+WONT = b'\xfc'
+DO = b'\xfd'
+DONT = b'\xfe'
+IAC = b'\xff' # Interpret As Command
+IAC_DOUBLED = b'\xff\xff'
+
+# selected telnet options
+BINARY = b'\x00' # 8-bit data path
+ECHO = b'\x01' # echo
+SGA = b'\x03' # suppress go ahead
+
+# RFC2217
+COM_PORT_OPTION = b'\x2c'
+
+# Client to Access Server
+SET_BAUDRATE = b'\x01'
+SET_DATASIZE = b'\x02'
+SET_PARITY = b'\x03'
+SET_STOPSIZE = b'\x04'
+SET_CONTROL = b'\x05'
+NOTIFY_LINESTATE = b'\x06'
+NOTIFY_MODEMSTATE = b'\x07'
+FLOWCONTROL_SUSPEND = b'\x08'
+FLOWCONTROL_RESUME = b'\x09'
+SET_LINESTATE_MASK = b'\x0a'
+SET_MODEMSTATE_MASK = b'\x0b'
+PURGE_DATA = b'\x0c'
+
+SERVER_SET_BAUDRATE = b'\x65'
+SERVER_SET_DATASIZE = b'\x66'
+SERVER_SET_PARITY = b'\x67'
+SERVER_SET_STOPSIZE = b'\x68'
+SERVER_SET_CONTROL = b'\x69'
+SERVER_NOTIFY_LINESTATE = b'\x6a'
+SERVER_NOTIFY_MODEMSTATE = b'\x6b'
+SERVER_FLOWCONTROL_SUSPEND = b'\x6c'
+SERVER_FLOWCONTROL_RESUME = b'\x6d'
+SERVER_SET_LINESTATE_MASK = b'\x6e'
+SERVER_SET_MODEMSTATE_MASK = b'\x6f'
+SERVER_PURGE_DATA = b'\x70'
+
+RFC2217_ANSWER_MAP = {
+ SET_BAUDRATE: SERVER_SET_BAUDRATE,
+ SET_DATASIZE: SERVER_SET_DATASIZE,
+ SET_PARITY: SERVER_SET_PARITY,
+ SET_STOPSIZE: SERVER_SET_STOPSIZE,
+ SET_CONTROL: SERVER_SET_CONTROL,
+ NOTIFY_LINESTATE: SERVER_NOTIFY_LINESTATE,
+ NOTIFY_MODEMSTATE: SERVER_NOTIFY_MODEMSTATE,
+ FLOWCONTROL_SUSPEND: SERVER_FLOWCONTROL_SUSPEND,
+ FLOWCONTROL_RESUME: SERVER_FLOWCONTROL_RESUME,
+ SET_LINESTATE_MASK: SERVER_SET_LINESTATE_MASK,
+ SET_MODEMSTATE_MASK: SERVER_SET_MODEMSTATE_MASK,
+ PURGE_DATA: SERVER_PURGE_DATA,
+}
+
+SET_CONTROL_REQ_FLOW_SETTING = b'\x00' # Request Com Port Flow Control Setting (outbound/both)
+SET_CONTROL_USE_NO_FLOW_CONTROL = b'\x01' # Use No Flow Control (outbound/both)
+SET_CONTROL_USE_SW_FLOW_CONTROL = b'\x02' # Use XON/XOFF Flow Control (outbound/both)
+SET_CONTROL_USE_HW_FLOW_CONTROL = b'\x03' # Use HARDWARE Flow Control (outbound/both)
+SET_CONTROL_REQ_BREAK_STATE = b'\x04' # Request BREAK State
+SET_CONTROL_BREAK_ON = b'\x05' # Set BREAK State ON
+SET_CONTROL_BREAK_OFF = b'\x06' # Set BREAK State OFF
+SET_CONTROL_REQ_DTR = b'\x07' # Request DTR Signal State
+SET_CONTROL_DTR_ON = b'\x08' # Set DTR Signal State ON
+SET_CONTROL_DTR_OFF = b'\x09' # Set DTR Signal State OFF
+SET_CONTROL_REQ_RTS = b'\x0a' # Request RTS Signal State
+SET_CONTROL_RTS_ON = b'\x0b' # Set RTS Signal State ON
+SET_CONTROL_RTS_OFF = b'\x0c' # Set RTS Signal State OFF
+SET_CONTROL_REQ_FLOW_SETTING_IN = b'\x0d' # Request Com Port Flow Control Setting (inbound)
+SET_CONTROL_USE_NO_FLOW_CONTROL_IN = b'\x0e' # Use No Flow Control (inbound)
+SET_CONTROL_USE_SW_FLOW_CONTOL_IN = b'\x0f' # Use XON/XOFF Flow Control (inbound)
+SET_CONTROL_USE_HW_FLOW_CONTOL_IN = b'\x10' # Use HARDWARE Flow Control (inbound)
+SET_CONTROL_USE_DCD_FLOW_CONTROL = b'\x11' # Use DCD Flow Control (outbound/both)
+SET_CONTROL_USE_DTR_FLOW_CONTROL = b'\x12' # Use DTR Flow Control (inbound)
+SET_CONTROL_USE_DSR_FLOW_CONTROL = b'\x13' # Use DSR Flow Control (outbound/both)
+
+LINESTATE_MASK_TIMEOUT = 128 # Time-out Error
+LINESTATE_MASK_SHIFTREG_EMPTY = 64 # Transfer Shift Register Empty
+LINESTATE_MASK_TRANSREG_EMPTY = 32 # Transfer Holding Register Empty
+LINESTATE_MASK_BREAK_DETECT = 16 # Break-detect Error
+LINESTATE_MASK_FRAMING_ERROR = 8 # Framing Error
+LINESTATE_MASK_PARTIY_ERROR = 4 # Parity Error
+LINESTATE_MASK_OVERRUN_ERROR = 2 # Overrun Error
+LINESTATE_MASK_DATA_READY = 1 # Data Ready
+
+MODEMSTATE_MASK_CD = 128 # Receive Line Signal Detect (also known as Carrier Detect)
+MODEMSTATE_MASK_RI = 64 # Ring Indicator
+MODEMSTATE_MASK_DSR = 32 # Data-Set-Ready Signal State
+MODEMSTATE_MASK_CTS = 16 # Clear-To-Send Signal State
+MODEMSTATE_MASK_CD_CHANGE = 8 # Delta Receive Line Signal Detect
+MODEMSTATE_MASK_RI_CHANGE = 4 # Trailing-edge Ring Detector
+MODEMSTATE_MASK_DSR_CHANGE = 2 # Delta Data-Set-Ready
+MODEMSTATE_MASK_CTS_CHANGE = 1 # Delta Clear-To-Send
+
+PURGE_RECEIVE_BUFFER = b'\x01' # Purge access server receive data buffer
+PURGE_TRANSMIT_BUFFER = b'\x02' # Purge access server transmit data buffer
+PURGE_BOTH_BUFFERS = b'\x03' # Purge both the access server receive data
+ # buffer and the access server transmit data buffer
+
+
+RFC2217_PARITY_MAP = {
+ serial.PARITY_NONE: 1,
+ serial.PARITY_ODD: 2,
+ serial.PARITY_EVEN: 3,
+ serial.PARITY_MARK: 4,
+ serial.PARITY_SPACE: 5,
+}
+RFC2217_REVERSE_PARITY_MAP = dict((v, k) for k, v in RFC2217_PARITY_MAP.items())
+
+RFC2217_STOPBIT_MAP = {
+ serial.STOPBITS_ONE: 1,
+ serial.STOPBITS_ONE_POINT_FIVE: 3,
+ serial.STOPBITS_TWO: 2,
+}
+RFC2217_REVERSE_STOPBIT_MAP = dict((v, k) for k, v in RFC2217_STOPBIT_MAP.items())
+
+# Telnet filter states
+M_NORMAL = 0
+M_IAC_SEEN = 1
+M_NEGOTIATE = 2
+
+# TelnetOption and TelnetSubnegotiation states
+REQUESTED = 'REQUESTED'
+ACTIVE = 'ACTIVE'
+INACTIVE = 'INACTIVE'
+REALLY_INACTIVE = 'REALLY_INACTIVE'
+
+
+class TelnetOption(object):
+ """Manage a single telnet option, keeps track of DO/DONT WILL/WONT."""
+
+ def __init__(self, connection, name, option, send_yes, send_no, ack_yes,
+ ack_no, initial_state, activation_callback=None):
+ """\
+ Initialize option.
+ :param connection: connection used to transmit answers
+ :param name: a readable name for debug outputs
+ :param send_yes: what to send when option is to be enabled.
+ :param send_no: what to send when option is to be disabled.
+ :param ack_yes: what to expect when remote agrees on option.
+ :param ack_no: what to expect when remote disagrees on option.
+ :param initial_state: options initialized with REQUESTED are tried to
+ be enabled on startup. use INACTIVE for all others.
+ """
+ self.connection = connection
+ self.name = name
+ self.option = option
+ self.send_yes = send_yes
+ self.send_no = send_no
+ self.ack_yes = ack_yes
+ self.ack_no = ack_no
+ self.state = initial_state
+ self.active = False
+ self.activation_callback = activation_callback
+
+ def __repr__(self):
+ """String for debug outputs"""
+ return "{o.name}:{o.active}({o.state})".format(o=self)
+
+ def process_incoming(self, command):
+ """\
+ A DO/DONT/WILL/WONT was received for this option, update state and
+ answer when needed.
+ """
+ if command == self.ack_yes:
+ if self.state is REQUESTED:
+ self.state = ACTIVE
+ self.active = True
+ if self.activation_callback is not None:
+ self.activation_callback()
+ elif self.state is ACTIVE:
+ pass
+ elif self.state is INACTIVE:
+ self.state = ACTIVE
+ self.connection.telnet_send_option(self.send_yes, self.option)
+ self.active = True
+ if self.activation_callback is not None:
+ self.activation_callback()
+ elif self.state is REALLY_INACTIVE:
+ self.connection.telnet_send_option(self.send_no, self.option)
+ else:
+ raise ValueError('option in illegal state {!r}'.format(self))
+ elif command == self.ack_no:
+ if self.state is REQUESTED:
+ self.state = INACTIVE
+ self.active = False
+ elif self.state is ACTIVE:
+ self.state = INACTIVE
+ self.connection.telnet_send_option(self.send_no, self.option)
+ self.active = False
+ elif self.state is INACTIVE:
+ pass
+ elif self.state is REALLY_INACTIVE:
+ pass
+ else:
+ raise ValueError('option in illegal state {!r}'.format(self))
+
+
+class TelnetSubnegotiation(object):
+ """\
+ A object to handle subnegotiation of options. In this case actually
+ sub-sub options for RFC 2217. It is used to track com port options.
+ """
+
+ def __init__(self, connection, name, option, ack_option=None):
+ if ack_option is None:
+ ack_option = option
+ self.connection = connection
+ self.name = name
+ self.option = option
+ self.value = None
+ self.ack_option = ack_option
+ self.state = INACTIVE
+
+ def __repr__(self):
+ """String for debug outputs."""
+ return "{sn.name}:{sn.state}".format(sn=self)
+
+ def set(self, value):
+ """\
+ Request a change of the value. a request is sent to the server. if
+ the client needs to know if the change is performed he has to check the
+ state of this object.
+ """
+ self.value = value
+ self.state = REQUESTED
+ self.connection.rfc2217_send_subnegotiation(self.option, self.value)
+ if self.connection.logger:
+ self.connection.logger.debug("SB Requesting {} -> {!r}".format(self.name, self.value))
+
+ def is_ready(self):
+ """\
+ Check if answer from server has been received. when server rejects
+ the change, raise a ValueError.
+ """
+ if self.state == REALLY_INACTIVE:
+ raise ValueError("remote rejected value for option {!r}".format(self.name))
+ return self.state == ACTIVE
+ # add property to have a similar interface as TelnetOption
+ active = property(is_ready)
+
+ def wait(self, timeout=3):
+ """\
+ Wait until the subnegotiation has been acknowledged or timeout. It
+ can also throw a value error when the answer from the server does not
+ match the value sent.
+ """
+ timeout_timer = Timeout(timeout)
+ while not timeout_timer.expired():
+ time.sleep(0.05) # prevent 100% CPU load
+ if self.is_ready():
+ break
+ else:
+ raise SerialException("timeout while waiting for option {!r}".format(self.name))
+
+ def check_answer(self, suboption):
+ """\
+ Check an incoming subnegotiation block. The parameter already has
+ cut off the header like sub option number and com port option value.
+ """
+ if self.value == suboption[:len(self.value)]:
+ self.state = ACTIVE
+ else:
+ # error propagation done in is_ready
+ self.state = REALLY_INACTIVE
+ if self.connection.logger:
+ self.connection.logger.debug("SB Answer {} -> {!r} -> {}".format(self.name, suboption, self.state))
+
+
+class Serial(SerialBase):
+ """Serial port implementation for RFC 2217 remote serial ports."""
+
+ BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
+ 9600, 19200, 38400, 57600, 115200)
+
+ def __init__(self, *args, **kwargs):
+ self._thread = None
+ self._socket = None
+ self._linestate = 0
+ self._modemstate = None
+ self._modemstate_timeout = Timeout(-1)
+ self._remote_suspend_flow = False
+ self._write_lock = None
+ self.logger = None
+ self._ignore_set_control_answer = False
+ self._poll_modem_state = False
+ self._network_timeout = 3
+ self._telnet_options = None
+ self._rfc2217_port_settings = None
+ self._rfc2217_options = None
+ self._read_buffer = None
+ super(Serial, self).__init__(*args, **kwargs) # must be last call in case of auto-open
+
+ def open(self):
+ """\
+ Open port with current settings. This may throw a SerialException
+ if the port cannot be opened.
+ """
+ self.logger = None
+ self._ignore_set_control_answer = False
+ self._poll_modem_state = False
+ self._network_timeout = 3
+ if self._port is None:
+ raise SerialException("Port must be configured before it can be used.")
+ if self.is_open:
+ raise SerialException("Port is already open.")
+ try:
+ self._socket = socket.create_connection(self.from_url(self.portstr), timeout=5) # XXX good value?
+ self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ except Exception as msg:
+ self._socket = None
+ raise SerialException("Could not open port {}: {}".format(self.portstr, msg))
+
+ # use a thread save queue as buffer. it also simplifies implementing
+ # the read timeout
+ self._read_buffer = Queue.Queue()
+ # to ensure that user writes does not interfere with internal
+ # telnet/rfc2217 options establish a lock
+ self._write_lock = threading.Lock()
+ # name the following separately so that, below, a check can be easily done
+ mandadory_options = [
+ TelnetOption(self, 'we-BINARY', BINARY, WILL, WONT, DO, DONT, INACTIVE),
+ TelnetOption(self, 'we-RFC2217', COM_PORT_OPTION, WILL, WONT, DO, DONT, REQUESTED),
+ ]
+ # all supported telnet options
+ self._telnet_options = [
+ TelnetOption(self, 'ECHO', ECHO, DO, DONT, WILL, WONT, REQUESTED),
+ TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED),
+ TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, REQUESTED),
+ TelnetOption(self, 'they-BINARY', BINARY, DO, DONT, WILL, WONT, INACTIVE),
+ TelnetOption(self, 'they-RFC2217', COM_PORT_OPTION, DO, DONT, WILL, WONT, REQUESTED),
+ ] + mandadory_options
+ # RFC 2217 specific states
+ # COM port settings
+ self._rfc2217_port_settings = {
+ 'baudrate': TelnetSubnegotiation(self, 'baudrate', SET_BAUDRATE, SERVER_SET_BAUDRATE),
+ 'datasize': TelnetSubnegotiation(self, 'datasize', SET_DATASIZE, SERVER_SET_DATASIZE),
+ 'parity': TelnetSubnegotiation(self, 'parity', SET_PARITY, SERVER_SET_PARITY),
+ 'stopsize': TelnetSubnegotiation(self, 'stopsize', SET_STOPSIZE, SERVER_SET_STOPSIZE),
+ }
+ # There are more subnegotiation objects, combine all in one dictionary
+ # for easy access
+ self._rfc2217_options = {
+ 'purge': TelnetSubnegotiation(self, 'purge', PURGE_DATA, SERVER_PURGE_DATA),
+ 'control': TelnetSubnegotiation(self, 'control', SET_CONTROL, SERVER_SET_CONTROL),
+ }
+ self._rfc2217_options.update(self._rfc2217_port_settings)
+ # cache for line and modem states that the server sends to us
+ self._linestate = 0
+ self._modemstate = None
+ self._modemstate_timeout = Timeout(-1)
+ # RFC 2217 flow control between server and client
+ self._remote_suspend_flow = False
+
+ self.is_open = True
+ self._thread = threading.Thread(target=self._telnet_read_loop)
+ self._thread.setDaemon(True)
+ self._thread.setName('pySerial RFC 2217 reader thread for {}'.format(self._port))
+ self._thread.start()
+
+ try: # must clean-up if open fails
+ # negotiate Telnet/RFC 2217 -> send initial requests
+ for option in self._telnet_options:
+ if option.state is REQUESTED:
+ self.telnet_send_option(option.send_yes, option.option)
+ # now wait until important options are negotiated
+ timeout = Timeout(self._network_timeout)
+ while not timeout.expired():
+ time.sleep(0.05) # prevent 100% CPU load
+ if sum(o.active for o in mandadory_options) == sum(o.state != INACTIVE for o in mandadory_options):
+ break
+ else:
+ raise SerialException(
+ "Remote does not seem to support RFC2217 or BINARY mode {!r}".format(mandadory_options))
+ if self.logger:
+ self.logger.info("Negotiated options: {}".format(self._telnet_options))
+
+ # fine, go on, set RFC 2217 specific things
+ self._reconfigure_port()
+ # all things set up get, now a clean start
+ if not self._dsrdtr:
+ self._update_dtr_state()
+ if not self._rtscts:
+ self._update_rts_state()
+ self.reset_input_buffer()
+ self.reset_output_buffer()
+ except:
+ self.close()
+ raise
+
+ def _reconfigure_port(self):
+ """Set communication parameters on opened port."""
+ if self._socket is None:
+ raise SerialException("Can only operate on open ports")
+
+ # if self._timeout != 0 and self._interCharTimeout is not None:
+ # XXX
+
+ if self._write_timeout is not None:
+ raise NotImplementedError('write_timeout is currently not supported')
+ # XXX
+
+ # Setup the connection
+ # to get good performance, all parameter changes are sent first...
+ if not 0 < self._baudrate < 2 ** 32:
+ raise ValueError("invalid baudrate: {!r}".format(self._baudrate))
+ self._rfc2217_port_settings['baudrate'].set(struct.pack(b'!I', self._baudrate))
+ self._rfc2217_port_settings['datasize'].set(struct.pack(b'!B', self._bytesize))
+ self._rfc2217_port_settings['parity'].set(struct.pack(b'!B', RFC2217_PARITY_MAP[self._parity]))
+ self._rfc2217_port_settings['stopsize'].set(struct.pack(b'!B', RFC2217_STOPBIT_MAP[self._stopbits]))
+
+ # and now wait until parameters are active
+ items = self._rfc2217_port_settings.values()
+ if self.logger:
+ self.logger.debug("Negotiating settings: {}".format(items))
+ timeout = Timeout(self._network_timeout)
+ while not timeout.expired():
+ time.sleep(0.05) # prevent 100% CPU load
+ if sum(o.active for o in items) == len(items):
+ break
+ else:
+ raise SerialException("Remote does not accept parameter change (RFC2217): {!r}".format(items))
+ if self.logger:
+ self.logger.info("Negotiated settings: {}".format(items))
+
+ if self._rtscts and self._xonxoff:
+ raise ValueError('xonxoff and rtscts together are not supported')
+ elif self._rtscts:
+ self.rfc2217_set_control(SET_CONTROL_USE_HW_FLOW_CONTROL)
+ elif self._xonxoff:
+ self.rfc2217_set_control(SET_CONTROL_USE_SW_FLOW_CONTROL)
+ else:
+ self.rfc2217_set_control(SET_CONTROL_USE_NO_FLOW_CONTROL)
+
+ def close(self):
+ """Close port"""
+ self.is_open = False
+ if self._socket:
+ try:
+ self._socket.shutdown(socket.SHUT_RDWR)
+ self._socket.close()
+ except:
+ # ignore errors.
+ pass
+ if self._thread:
+ self._thread.join(7) # XXX more than socket timeout
+ self._thread = None
+ # in case of quick reconnects, give the server some time
+ time.sleep(0.3)
+ self._socket = None
+
+ def from_url(self, url):
+ """\
+ extract host and port from an URL string, other settings are extracted
+ an stored in instance
+ """
+ parts = urlparse.urlsplit(url)
+ if parts.scheme != "rfc2217":
+ raise SerialException(
+ 'expected a string in the form '
+ '"rfc2217://:[?option[&option...]]": '
+ 'not starting with rfc2217:// ({!r})'.format(parts.scheme))
+ try:
+ # process options now, directly altering self
+ for option, values in urlparse.parse_qs(parts.query, True).items():
+ if option == 'logging':
+ logging.basicConfig() # XXX is that good to call it here?
+ self.logger = logging.getLogger('pySerial.rfc2217')
+ self.logger.setLevel(LOGGER_LEVELS[values[0]])
+ self.logger.debug('enabled logging')
+ elif option == 'ign_set_control':
+ self._ignore_set_control_answer = True
+ elif option == 'poll_modem':
+ self._poll_modem_state = True
+ elif option == 'timeout':
+ self._network_timeout = float(values[0])
+ else:
+ raise ValueError('unknown option: {!r}'.format(option))
+ if not 0 <= parts.port < 65536:
+ raise ValueError("port not in range 0...65535")
+ except ValueError as e:
+ raise SerialException(
+ 'expected a string in the form '
+ '"rfc2217://:[?option[&option...]]": {}'.format(e))
+ return (parts.hostname, parts.port)
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+
+ @property
+ def in_waiting(self):
+ """Return the number of bytes currently in the input buffer."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ return self._read_buffer.qsize()
+
+ def read(self, size=1):
+ """\
+ Read size bytes from the serial port. If a timeout is set it may
+ return less characters as requested. With no timeout it will block
+ until the requested number of bytes is read.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ data = bytearray()
+ try:
+ timeout = Timeout(self._timeout)
+ while len(data) < size:
+ if self._thread is None or not self._thread.is_alive():
+ raise SerialException('connection failed (reader thread died)')
+ buf = self._read_buffer.get(True, timeout.time_left())
+ if buf is None:
+ return bytes(data)
+ data += buf
+ if timeout.expired():
+ break
+ except Queue.Empty: # -> timeout
+ pass
+ return bytes(data)
+
+ def write(self, data):
+ """\
+ Output the given byte string over the serial port. Can block if the
+ connection is blocked. May raise SerialException if the connection is
+ closed.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ with self._write_lock:
+ try:
+ self._socket.sendall(to_bytes(data).replace(IAC, IAC_DOUBLED))
+ except socket.error as e:
+ raise SerialException("connection failed (socket error): {}".format(e))
+ return len(data)
+
+ def reset_input_buffer(self):
+ """Clear input buffer, discarding all that is in the buffer."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ self.rfc2217_send_purge(PURGE_RECEIVE_BUFFER)
+ # empty read buffer
+ while self._read_buffer.qsize():
+ self._read_buffer.get(False)
+
+ def reset_output_buffer(self):
+ """\
+ Clear output buffer, aborting the current output and
+ discarding all that is in the buffer.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ self.rfc2217_send_purge(PURGE_TRANSMIT_BUFFER)
+
+ def _update_break_state(self):
+ """\
+ Set break: Controls TXD. When active, to transmitting is
+ possible.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ if self.logger:
+ self.logger.info('set BREAK to {}'.format('active' if self._break_state else 'inactive'))
+ if self._break_state:
+ self.rfc2217_set_control(SET_CONTROL_BREAK_ON)
+ else:
+ self.rfc2217_set_control(SET_CONTROL_BREAK_OFF)
+
+ def _update_rts_state(self):
+ """Set terminal status line: Request To Send."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ if self.logger:
+ self.logger.info('set RTS to {}'.format('active' if self._rts_state else 'inactive'))
+ if self._rts_state:
+ self.rfc2217_set_control(SET_CONTROL_RTS_ON)
+ else:
+ self.rfc2217_set_control(SET_CONTROL_RTS_OFF)
+
+ def _update_dtr_state(self):
+ """Set terminal status line: Data Terminal Ready."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ if self.logger:
+ self.logger.info('set DTR to {}'.format('active' if self._dtr_state else 'inactive'))
+ if self._dtr_state:
+ self.rfc2217_set_control(SET_CONTROL_DTR_ON)
+ else:
+ self.rfc2217_set_control(SET_CONTROL_DTR_OFF)
+
+ @property
+ def cts(self):
+ """Read terminal status line: Clear To Send."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ return bool(self.get_modem_state() & MODEMSTATE_MASK_CTS)
+
+ @property
+ def dsr(self):
+ """Read terminal status line: Data Set Ready."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ return bool(self.get_modem_state() & MODEMSTATE_MASK_DSR)
+
+ @property
+ def ri(self):
+ """Read terminal status line: Ring Indicator."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ return bool(self.get_modem_state() & MODEMSTATE_MASK_RI)
+
+ @property
+ def cd(self):
+ """Read terminal status line: Carrier Detect."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ return bool(self.get_modem_state() & MODEMSTATE_MASK_CD)
+
+ # - - - platform specific - - -
+ # None so far
+
+ # - - - RFC2217 specific - - -
+
+ def _telnet_read_loop(self):
+ """Read loop for the socket."""
+ mode = M_NORMAL
+ suboption = None
+ try:
+ while self.is_open:
+ try:
+ data = self._socket.recv(1024)
+ except socket.timeout:
+ # just need to get out of recv form time to time to check if
+ # still alive
+ continue
+ except socket.error as e:
+ # connection fails -> terminate loop
+ if self.logger:
+ self.logger.debug("socket error in reader thread: {}".format(e))
+ self._read_buffer.put(None)
+ break
+ if not data:
+ self._read_buffer.put(None)
+ break # lost connection
+ for byte in iterbytes(data):
+ if mode == M_NORMAL:
+ # interpret as command or as data
+ if byte == IAC:
+ mode = M_IAC_SEEN
+ else:
+ # store data in read buffer or sub option buffer
+ # depending on state
+ if suboption is not None:
+ suboption += byte
+ else:
+ self._read_buffer.put(byte)
+ elif mode == M_IAC_SEEN:
+ if byte == IAC:
+ # interpret as command doubled -> insert character
+ # itself
+ if suboption is not None:
+ suboption += IAC
+ else:
+ self._read_buffer.put(IAC)
+ mode = M_NORMAL
+ elif byte == SB:
+ # sub option start
+ suboption = bytearray()
+ mode = M_NORMAL
+ elif byte == SE:
+ # sub option end -> process it now
+ self._telnet_process_subnegotiation(bytes(suboption))
+ suboption = None
+ mode = M_NORMAL
+ elif byte in (DO, DONT, WILL, WONT):
+ # negotiation
+ telnet_command = byte
+ mode = M_NEGOTIATE
+ else:
+ # other telnet commands
+ self._telnet_process_command(byte)
+ mode = M_NORMAL
+ elif mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following
+ self._telnet_negotiate_option(telnet_command, byte)
+ mode = M_NORMAL
+ finally:
+ if self.logger:
+ self.logger.debug("read thread terminated")
+
+ # - incoming telnet commands and options
+
+ def _telnet_process_command(self, command):
+ """Process commands other than DO, DONT, WILL, WONT."""
+ # Currently none. RFC2217 only uses negotiation and subnegotiation.
+ if self.logger:
+ self.logger.warning("ignoring Telnet command: {!r}".format(command))
+
+ def _telnet_negotiate_option(self, command, option):
+ """Process incoming DO, DONT, WILL, WONT."""
+ # check our registered telnet options and forward command to them
+ # they know themselves if they have to answer or not
+ known = False
+ for item in self._telnet_options:
+ # can have more than one match! as some options are duplicated for
+ # 'us' and 'them'
+ if item.option == option:
+ item.process_incoming(command)
+ known = True
+ if not known:
+ # handle unknown options
+ # only answer to positive requests and deny them
+ if command == WILL or command == DO:
+ self.telnet_send_option((DONT if command == WILL else WONT), option)
+ if self.logger:
+ self.logger.warning("rejected Telnet option: {!r}".format(option))
+
+ def _telnet_process_subnegotiation(self, suboption):
+ """Process subnegotiation, the data between IAC SB and IAC SE."""
+ if suboption[0:1] == COM_PORT_OPTION:
+ if suboption[1:2] == SERVER_NOTIFY_LINESTATE and len(suboption) >= 3:
+ self._linestate = ord(suboption[2:3]) # ensure it is a number
+ if self.logger:
+ self.logger.info("NOTIFY_LINESTATE: {}".format(self._linestate))
+ elif suboption[1:2] == SERVER_NOTIFY_MODEMSTATE and len(suboption) >= 3:
+ self._modemstate = ord(suboption[2:3]) # ensure it is a number
+ if self.logger:
+ self.logger.info("NOTIFY_MODEMSTATE: {}".format(self._modemstate))
+ # update time when we think that a poll would make sense
+ self._modemstate_timeout.restart(0.3)
+ elif suboption[1:2] == FLOWCONTROL_SUSPEND:
+ self._remote_suspend_flow = True
+ elif suboption[1:2] == FLOWCONTROL_RESUME:
+ self._remote_suspend_flow = False
+ else:
+ for item in self._rfc2217_options.values():
+ if item.ack_option == suboption[1:2]:
+ #~ print "processing COM_PORT_OPTION: %r" % list(suboption[1:])
+ item.check_answer(bytes(suboption[2:]))
+ break
+ else:
+ if self.logger:
+ self.logger.warning("ignoring COM_PORT_OPTION: {!r}".format(suboption))
+ else:
+ if self.logger:
+ self.logger.warning("ignoring subnegotiation: {!r}".format(suboption))
+
+ # - outgoing telnet commands and options
+
+ def _internal_raw_write(self, data):
+ """internal socket write with no data escaping. used to send telnet stuff."""
+ with self._write_lock:
+ self._socket.sendall(data)
+
+ def telnet_send_option(self, action, option):
+ """Send DO, DONT, WILL, WONT."""
+ self._internal_raw_write(IAC + action + option)
+
+ def rfc2217_send_subnegotiation(self, option, value=b''):
+ """Subnegotiation of RFC2217 parameters."""
+ value = value.replace(IAC, IAC_DOUBLED)
+ self._internal_raw_write(IAC + SB + COM_PORT_OPTION + option + value + IAC + SE)
+
+ def rfc2217_send_purge(self, value):
+ """\
+ Send purge request to the remote.
+ (PURGE_RECEIVE_BUFFER / PURGE_TRANSMIT_BUFFER / PURGE_BOTH_BUFFERS)
+ """
+ item = self._rfc2217_options['purge']
+ item.set(value) # transmit desired purge type
+ item.wait(self._network_timeout) # wait for acknowledge from the server
+
+ def rfc2217_set_control(self, value):
+ """transmit change of control line to remote"""
+ item = self._rfc2217_options['control']
+ item.set(value) # transmit desired control type
+ if self._ignore_set_control_answer:
+ # answers are ignored when option is set. compatibility mode for
+ # servers that answer, but not the expected one... (or no answer
+ # at all) i.e. sredird
+ time.sleep(0.1) # this helps getting the unit tests passed
+ else:
+ item.wait(self._network_timeout) # wait for acknowledge from the server
+
+ def rfc2217_flow_server_ready(self):
+ """\
+ check if server is ready to receive data. block for some time when
+ not.
+ """
+ #~ if self._remote_suspend_flow:
+ #~ wait---
+
+ def get_modem_state(self):
+ """\
+ get last modem state (cached value. If value is "old", request a new
+ one. This cache helps that we don't issue to many requests when e.g. all
+ status lines, one after the other is queried by the user (CTS, DSR
+ etc.)
+ """
+ # active modem state polling enabled? is the value fresh enough?
+ if self._poll_modem_state and self._modemstate_timeout.expired():
+ if self.logger:
+ self.logger.debug('polling modem state')
+ # when it is older, request an update
+ self.rfc2217_send_subnegotiation(NOTIFY_MODEMSTATE)
+ timeout = Timeout(self._network_timeout)
+ while not timeout.expired():
+ time.sleep(0.05) # prevent 100% CPU load
+ # when expiration time is updated, it means that there is a new
+ # value
+ if not self._modemstate_timeout.expired():
+ break
+ else:
+ if self.logger:
+ self.logger.warning('poll for modem state failed')
+ # even when there is a timeout, do not generate an error just
+ # return the last known value. this way we can support buggy
+ # servers that do not respond to polls, but send automatic
+ # updates.
+ if self._modemstate is not None:
+ if self.logger:
+ self.logger.debug('using cached modem state')
+ return self._modemstate
+ else:
+ # never received a notification from the server
+ raise SerialException("remote sends no NOTIFY_MODEMSTATE")
+
+
+#############################################################################
+# The following is code that helps implementing an RFC 2217 server.
+
+class PortManager(object):
+ """\
+ This class manages the state of Telnet and RFC 2217. It needs a serial
+ instance and a connection to work with. Connection is expected to implement
+ a (thread safe) write function, that writes the string to the network.
+ """
+
+ def __init__(self, serial_port, connection, logger=None):
+ self.serial = serial_port
+ self.connection = connection
+ self.logger = logger
+ self._client_is_rfc2217 = False
+
+ # filter state machine
+ self.mode = M_NORMAL
+ self.suboption = None
+ self.telnet_command = None
+
+ # states for modem/line control events
+ self.modemstate_mask = 255
+ self.last_modemstate = None
+ self.linstate_mask = 0
+
+ # all supported telnet options
+ self._telnet_options = [
+ TelnetOption(self, 'ECHO', ECHO, WILL, WONT, DO, DONT, REQUESTED),
+ TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED),
+ TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, INACTIVE),
+ TelnetOption(self, 'we-BINARY', BINARY, WILL, WONT, DO, DONT, INACTIVE),
+ TelnetOption(self, 'they-BINARY', BINARY, DO, DONT, WILL, WONT, REQUESTED),
+ TelnetOption(self, 'we-RFC2217', COM_PORT_OPTION, WILL, WONT, DO, DONT, REQUESTED, self._client_ok),
+ TelnetOption(self, 'they-RFC2217', COM_PORT_OPTION, DO, DONT, WILL, WONT, INACTIVE, self._client_ok),
+ ]
+
+ # negotiate Telnet/RFC2217 -> send initial requests
+ if self.logger:
+ self.logger.debug("requesting initial Telnet/RFC 2217 options")
+ for option in self._telnet_options:
+ if option.state is REQUESTED:
+ self.telnet_send_option(option.send_yes, option.option)
+ # issue 1st modem state notification
+
+ def _client_ok(self):
+ """\
+ callback of telnet option. It gets called when option is activated.
+ This one here is used to detect when the client agrees on RFC 2217. A
+ flag is set so that other functions like check_modem_lines know if the
+ client is OK.
+ """
+ # The callback is used for we and they so if one party agrees, we're
+ # already happy. it seems not all servers do the negotiation correctly
+ # and i guess there are incorrect clients too.. so be happy if client
+ # answers one or the other positively.
+ self._client_is_rfc2217 = True
+ if self.logger:
+ self.logger.info("client accepts RFC 2217")
+ # this is to ensure that the client gets a notification, even if there
+ # was no change
+ self.check_modem_lines(force_notification=True)
+
+ # - outgoing telnet commands and options
+
+ def telnet_send_option(self, action, option):
+ """Send DO, DONT, WILL, WONT."""
+ self.connection.write(IAC + action + option)
+
+ def rfc2217_send_subnegotiation(self, option, value=b''):
+ """Subnegotiation of RFC 2217 parameters."""
+ value = value.replace(IAC, IAC_DOUBLED)
+ self.connection.write(IAC + SB + COM_PORT_OPTION + option + value + IAC + SE)
+
+ # - check modem lines, needs to be called periodically from user to
+ # establish polling
+
+ def check_modem_lines(self, force_notification=False):
+ """\
+ read control lines from serial port and compare the last value sent to remote.
+ send updates on changes.
+ """
+ modemstate = (
+ (self.serial.cts and MODEMSTATE_MASK_CTS) |
+ (self.serial.dsr and MODEMSTATE_MASK_DSR) |
+ (self.serial.ri and MODEMSTATE_MASK_RI) |
+ (self.serial.cd and MODEMSTATE_MASK_CD))
+ # check what has changed
+ deltas = modemstate ^ (self.last_modemstate or 0) # when last is None -> 0
+ if deltas & MODEMSTATE_MASK_CTS:
+ modemstate |= MODEMSTATE_MASK_CTS_CHANGE
+ if deltas & MODEMSTATE_MASK_DSR:
+ modemstate |= MODEMSTATE_MASK_DSR_CHANGE
+ if deltas & MODEMSTATE_MASK_RI:
+ modemstate |= MODEMSTATE_MASK_RI_CHANGE
+ if deltas & MODEMSTATE_MASK_CD:
+ modemstate |= MODEMSTATE_MASK_CD_CHANGE
+ # if new state is different and the mask allows this change, send
+ # notification. suppress notifications when client is not rfc2217
+ if modemstate != self.last_modemstate or force_notification:
+ if (self._client_is_rfc2217 and (modemstate & self.modemstate_mask)) or force_notification:
+ self.rfc2217_send_subnegotiation(
+ SERVER_NOTIFY_MODEMSTATE,
+ to_bytes([modemstate & self.modemstate_mask]))
+ if self.logger:
+ self.logger.info("NOTIFY_MODEMSTATE: {}".format(modemstate))
+ # save last state, but forget about deltas.
+ # otherwise it would also notify about changing deltas which is
+ # probably not very useful
+ self.last_modemstate = modemstate & 0xf0
+
+ # - outgoing data escaping
+
+ def escape(self, data):
+ """\
+ This generator function is for the user. All outgoing data has to be
+ properly escaped, so that no IAC character in the data stream messes up
+ the Telnet state machine in the server.
+
+ socket.sendall(escape(data))
+ """
+ for byte in iterbytes(data):
+ if byte == IAC:
+ yield IAC
+ yield IAC
+ else:
+ yield byte
+
+ # - incoming data filter
+
+ def filter(self, data):
+ """\
+ Handle a bunch of incoming bytes. This is a generator. It will yield
+ all characters not of interest for Telnet/RFC 2217.
+
+ The idea is that the reader thread pushes data from the socket through
+ this filter:
+
+ for byte in filter(socket.recv(1024)):
+ # do things like CR/LF conversion/whatever
+ # and write data to the serial port
+ serial.write(byte)
+
+ (socket error handling code left as exercise for the reader)
+ """
+ for byte in iterbytes(data):
+ if self.mode == M_NORMAL:
+ # interpret as command or as data
+ if byte == IAC:
+ self.mode = M_IAC_SEEN
+ else:
+ # store data in sub option buffer or pass it to our
+ # consumer depending on state
+ if self.suboption is not None:
+ self.suboption += byte
+ else:
+ yield byte
+ elif self.mode == M_IAC_SEEN:
+ if byte == IAC:
+ # interpret as command doubled -> insert character
+ # itself
+ if self.suboption is not None:
+ self.suboption += byte
+ else:
+ yield byte
+ self.mode = M_NORMAL
+ elif byte == SB:
+ # sub option start
+ self.suboption = bytearray()
+ self.mode = M_NORMAL
+ elif byte == SE:
+ # sub option end -> process it now
+ self._telnet_process_subnegotiation(bytes(self.suboption))
+ self.suboption = None
+ self.mode = M_NORMAL
+ elif byte in (DO, DONT, WILL, WONT):
+ # negotiation
+ self.telnet_command = byte
+ self.mode = M_NEGOTIATE
+ else:
+ # other telnet commands
+ self._telnet_process_command(byte)
+ self.mode = M_NORMAL
+ elif self.mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following
+ self._telnet_negotiate_option(self.telnet_command, byte)
+ self.mode = M_NORMAL
+
+ # - incoming telnet commands and options
+
+ def _telnet_process_command(self, command):
+ """Process commands other than DO, DONT, WILL, WONT."""
+ # Currently none. RFC2217 only uses negotiation and subnegotiation.
+ if self.logger:
+ self.logger.warning("ignoring Telnet command: {!r}".format(command))
+
+ def _telnet_negotiate_option(self, command, option):
+ """Process incoming DO, DONT, WILL, WONT."""
+ # check our registered telnet options and forward command to them
+ # they know themselves if they have to answer or not
+ known = False
+ for item in self._telnet_options:
+ # can have more than one match! as some options are duplicated for
+ # 'us' and 'them'
+ if item.option == option:
+ item.process_incoming(command)
+ known = True
+ if not known:
+ # handle unknown options
+ # only answer to positive requests and deny them
+ if command == WILL or command == DO:
+ self.telnet_send_option((DONT if command == WILL else WONT), option)
+ if self.logger:
+ self.logger.warning("rejected Telnet option: {!r}".format(option))
+
+ def _telnet_process_subnegotiation(self, suboption):
+ """Process subnegotiation, the data between IAC SB and IAC SE."""
+ if suboption[0:1] == COM_PORT_OPTION:
+ if self.logger:
+ self.logger.debug('received COM_PORT_OPTION: {!r}'.format(suboption))
+ if suboption[1:2] == SET_BAUDRATE:
+ backup = self.serial.baudrate
+ try:
+ (baudrate,) = struct.unpack(b"!I", suboption[2:6])
+ if baudrate != 0:
+ self.serial.baudrate = baudrate
+ except ValueError as e:
+ if self.logger:
+ self.logger.error("failed to set baud rate: {}".format(e))
+ self.serial.baudrate = backup
+ else:
+ if self.logger:
+ self.logger.info("{} baud rate: {}".format('set' if baudrate else 'get', self.serial.baudrate))
+ self.rfc2217_send_subnegotiation(SERVER_SET_BAUDRATE, struct.pack(b"!I", self.serial.baudrate))
+ elif suboption[1:2] == SET_DATASIZE:
+ backup = self.serial.bytesize
+ try:
+ (datasize,) = struct.unpack(b"!B", suboption[2:3])
+ if datasize != 0:
+ self.serial.bytesize = datasize
+ except ValueError as e:
+ if self.logger:
+ self.logger.error("failed to set data size: {}".format(e))
+ self.serial.bytesize = backup
+ else:
+ if self.logger:
+ self.logger.info("{} data size: {}".format('set' if datasize else 'get', self.serial.bytesize))
+ self.rfc2217_send_subnegotiation(SERVER_SET_DATASIZE, struct.pack(b"!B", self.serial.bytesize))
+ elif suboption[1:2] == SET_PARITY:
+ backup = self.serial.parity
+ try:
+ parity = struct.unpack(b"!B", suboption[2:3])[0]
+ if parity != 0:
+ self.serial.parity = RFC2217_REVERSE_PARITY_MAP[parity]
+ except ValueError as e:
+ if self.logger:
+ self.logger.error("failed to set parity: {}".format(e))
+ self.serial.parity = backup
+ else:
+ if self.logger:
+ self.logger.info("{} parity: {}".format('set' if parity else 'get', self.serial.parity))
+ self.rfc2217_send_subnegotiation(
+ SERVER_SET_PARITY,
+ struct.pack(b"!B", RFC2217_PARITY_MAP[self.serial.parity]))
+ elif suboption[1:2] == SET_STOPSIZE:
+ backup = self.serial.stopbits
+ try:
+ stopbits = struct.unpack(b"!B", suboption[2:3])[0]
+ if stopbits != 0:
+ self.serial.stopbits = RFC2217_REVERSE_STOPBIT_MAP[stopbits]
+ except ValueError as e:
+ if self.logger:
+ self.logger.error("failed to set stop bits: {}".format(e))
+ self.serial.stopbits = backup
+ else:
+ if self.logger:
+ self.logger.info("{} stop bits: {}".format('set' if stopbits else 'get', self.serial.stopbits))
+ self.rfc2217_send_subnegotiation(
+ SERVER_SET_STOPSIZE,
+ struct.pack(b"!B", RFC2217_STOPBIT_MAP[self.serial.stopbits]))
+ elif suboption[1:2] == SET_CONTROL:
+ if suboption[2:3] == SET_CONTROL_REQ_FLOW_SETTING:
+ if self.serial.xonxoff:
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_SW_FLOW_CONTROL)
+ elif self.serial.rtscts:
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_HW_FLOW_CONTROL)
+ else:
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_NO_FLOW_CONTROL)
+ elif suboption[2:3] == SET_CONTROL_USE_NO_FLOW_CONTROL:
+ self.serial.xonxoff = False
+ self.serial.rtscts = False
+ if self.logger:
+ self.logger.info("changed flow control to None")
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_NO_FLOW_CONTROL)
+ elif suboption[2:3] == SET_CONTROL_USE_SW_FLOW_CONTROL:
+ self.serial.xonxoff = True
+ if self.logger:
+ self.logger.info("changed flow control to XON/XOFF")
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_SW_FLOW_CONTROL)
+ elif suboption[2:3] == SET_CONTROL_USE_HW_FLOW_CONTROL:
+ self.serial.rtscts = True
+ if self.logger:
+ self.logger.info("changed flow control to RTS/CTS")
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_HW_FLOW_CONTROL)
+ elif suboption[2:3] == SET_CONTROL_REQ_BREAK_STATE:
+ if self.logger:
+ self.logger.warning("requested break state - not implemented")
+ pass # XXX needs cached value
+ elif suboption[2:3] == SET_CONTROL_BREAK_ON:
+ self.serial.break_condition = True
+ if self.logger:
+ self.logger.info("changed BREAK to active")
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_BREAK_ON)
+ elif suboption[2:3] == SET_CONTROL_BREAK_OFF:
+ self.serial.break_condition = False
+ if self.logger:
+ self.logger.info("changed BREAK to inactive")
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_BREAK_OFF)
+ elif suboption[2:3] == SET_CONTROL_REQ_DTR:
+ if self.logger:
+ self.logger.warning("requested DTR state - not implemented")
+ pass # XXX needs cached value
+ elif suboption[2:3] == SET_CONTROL_DTR_ON:
+ self.serial.dtr = True
+ if self.logger:
+ self.logger.info("changed DTR to active")
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_DTR_ON)
+ elif suboption[2:3] == SET_CONTROL_DTR_OFF:
+ self.serial.dtr = False
+ if self.logger:
+ self.logger.info("changed DTR to inactive")
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_DTR_OFF)
+ elif suboption[2:3] == SET_CONTROL_REQ_RTS:
+ if self.logger:
+ self.logger.warning("requested RTS state - not implemented")
+ pass # XXX needs cached value
+ #~ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_ON)
+ elif suboption[2:3] == SET_CONTROL_RTS_ON:
+ self.serial.rts = True
+ if self.logger:
+ self.logger.info("changed RTS to active")
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_ON)
+ elif suboption[2:3] == SET_CONTROL_RTS_OFF:
+ self.serial.rts = False
+ if self.logger:
+ self.logger.info("changed RTS to inactive")
+ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_OFF)
+ #~ elif suboption[2:3] == SET_CONTROL_REQ_FLOW_SETTING_IN:
+ #~ elif suboption[2:3] == SET_CONTROL_USE_NO_FLOW_CONTROL_IN:
+ #~ elif suboption[2:3] == SET_CONTROL_USE_SW_FLOW_CONTOL_IN:
+ #~ elif suboption[2:3] == SET_CONTROL_USE_HW_FLOW_CONTOL_IN:
+ #~ elif suboption[2:3] == SET_CONTROL_USE_DCD_FLOW_CONTROL:
+ #~ elif suboption[2:3] == SET_CONTROL_USE_DTR_FLOW_CONTROL:
+ #~ elif suboption[2:3] == SET_CONTROL_USE_DSR_FLOW_CONTROL:
+ elif suboption[1:2] == NOTIFY_LINESTATE:
+ # client polls for current state
+ self.rfc2217_send_subnegotiation(
+ SERVER_NOTIFY_LINESTATE,
+ to_bytes([0])) # sorry, nothing like that implemented
+ elif suboption[1:2] == NOTIFY_MODEMSTATE:
+ if self.logger:
+ self.logger.info("request for modem state")
+ # client polls for current state
+ self.check_modem_lines(force_notification=True)
+ elif suboption[1:2] == FLOWCONTROL_SUSPEND:
+ if self.logger:
+ self.logger.info("suspend")
+ self._remote_suspend_flow = True
+ elif suboption[1:2] == FLOWCONTROL_RESUME:
+ if self.logger:
+ self.logger.info("resume")
+ self._remote_suspend_flow = False
+ elif suboption[1:2] == SET_LINESTATE_MASK:
+ self.linstate_mask = ord(suboption[2:3]) # ensure it is a number
+ if self.logger:
+ self.logger.info("line state mask: 0x{:02x}".format(self.linstate_mask))
+ elif suboption[1:2] == SET_MODEMSTATE_MASK:
+ self.modemstate_mask = ord(suboption[2:3]) # ensure it is a number
+ if self.logger:
+ self.logger.info("modem state mask: 0x{:02x}".format(self.modemstate_mask))
+ elif suboption[1:2] == PURGE_DATA:
+ if suboption[2:3] == PURGE_RECEIVE_BUFFER:
+ self.serial.reset_input_buffer()
+ if self.logger:
+ self.logger.info("purge in")
+ self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_RECEIVE_BUFFER)
+ elif suboption[2:3] == PURGE_TRANSMIT_BUFFER:
+ self.serial.reset_output_buffer()
+ if self.logger:
+ self.logger.info("purge out")
+ self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_TRANSMIT_BUFFER)
+ elif suboption[2:3] == PURGE_BOTH_BUFFERS:
+ self.serial.reset_input_buffer()
+ self.serial.reset_output_buffer()
+ if self.logger:
+ self.logger.info("purge both")
+ self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_BOTH_BUFFERS)
+ else:
+ if self.logger:
+ self.logger.error("undefined PURGE_DATA: {!r}".format(list(suboption[2:])))
+ else:
+ if self.logger:
+ self.logger.error("undefined COM_PORT_OPTION: {!r}".format(list(suboption[1:])))
+ else:
+ if self.logger:
+ self.logger.warning("unknown subnegotiation: {!r}".format(suboption))
+
+
+# simple client test
+if __name__ == '__main__':
+ import sys
+ s = Serial('rfc2217://localhost:7000', 115200)
+ sys.stdout.write('{}\n'.format(s))
+
+ sys.stdout.write("write...\n")
+ s.write(b"hello\n")
+ s.flush()
+ sys.stdout.write("read: {}\n".format(s.read(5)))
+ s.close()
diff --git a/mixly/tools/python/serial/rs485.py b/mixly/tools/python/serial/rs485.py
new file mode 100644
index 00000000..d7aff6f6
--- /dev/null
+++ b/mixly/tools/python/serial/rs485.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+
+# RS485 support
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2015 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""\
+The settings for RS485 are stored in a dedicated object that can be applied to
+serial ports (where supported).
+NOTE: Some implementations may only support a subset of the settings.
+"""
+
+from __future__ import absolute_import
+
+import time
+import serial
+
+
+class RS485Settings(object):
+ def __init__(
+ self,
+ rts_level_for_tx=True,
+ rts_level_for_rx=False,
+ loopback=False,
+ delay_before_tx=None,
+ delay_before_rx=None):
+ self.rts_level_for_tx = rts_level_for_tx
+ self.rts_level_for_rx = rts_level_for_rx
+ self.loopback = loopback
+ self.delay_before_tx = delay_before_tx
+ self.delay_before_rx = delay_before_rx
+
+
+class RS485(serial.Serial):
+ """\
+ A subclass that replaces the write method with one that toggles RTS
+ according to the RS485 settings.
+
+ NOTE: This may work unreliably on some serial ports (control signals not
+ synchronized or delayed compared to data). Using delays may be
+ unreliable (varying times, larger than expected) as the OS may not
+ support very fine grained delays (no smaller than in the order of
+ tens of milliseconds).
+
+ NOTE: Some implementations support this natively. Better performance
+ can be expected when the native version is used.
+
+ NOTE: The loopback property is ignored by this implementation. The actual
+ behavior depends on the used hardware.
+
+ Usage:
+
+ ser = RS485(...)
+ ser.rs485_mode = RS485Settings(...)
+ ser.write(b'hello')
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(RS485, self).__init__(*args, **kwargs)
+ self._alternate_rs485_settings = None
+
+ def write(self, b):
+ """Write to port, controlling RTS before and after transmitting."""
+ if self._alternate_rs485_settings is not None:
+ # apply level for TX and optional delay
+ self.setRTS(self._alternate_rs485_settings.rts_level_for_tx)
+ if self._alternate_rs485_settings.delay_before_tx is not None:
+ time.sleep(self._alternate_rs485_settings.delay_before_tx)
+ # write and wait for data to be written
+ super(RS485, self).write(b)
+ super(RS485, self).flush()
+ # optional delay and apply level for RX
+ if self._alternate_rs485_settings.delay_before_rx is not None:
+ time.sleep(self._alternate_rs485_settings.delay_before_rx)
+ self.setRTS(self._alternate_rs485_settings.rts_level_for_rx)
+ else:
+ super(RS485, self).write(b)
+
+ # redirect where the property stores the settings so that underlying Serial
+ # instance does not see them
+ @property
+ def rs485_mode(self):
+ """\
+ Enable RS485 mode and apply new settings, set to None to disable.
+ See serial.rs485.RS485Settings for more info about the value.
+ """
+ return self._alternate_rs485_settings
+
+ @rs485_mode.setter
+ def rs485_mode(self, rs485_settings):
+ self._alternate_rs485_settings = rs485_settings
diff --git a/mixly/tools/python/serial/serialcli.py b/mixly/tools/python/serial/serialcli.py
new file mode 100644
index 00000000..4614736e
--- /dev/null
+++ b/mixly/tools/python/serial/serialcli.py
@@ -0,0 +1,253 @@
+#! python
+#
+# Backend for .NET/Mono (IronPython), .NET >= 2
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2008-2015 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import absolute_import
+
+import System
+import System.IO.Ports
+from serial.serialutil import *
+
+# must invoke function with byte array, make a helper to convert strings
+# to byte arrays
+sab = System.Array[System.Byte]
+
+
+def as_byte_array(string):
+ return sab([ord(x) for x in string]) # XXX will require adaption when run with a 3.x compatible IronPython
+
+
+class Serial(SerialBase):
+ """Serial port implementation for .NET/Mono."""
+
+ BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
+ 9600, 19200, 38400, 57600, 115200)
+
+ def open(self):
+ """\
+ Open port with current settings. This may throw a SerialException
+ if the port cannot be opened.
+ """
+ if self._port is None:
+ raise SerialException("Port must be configured before it can be used.")
+ if self.is_open:
+ raise SerialException("Port is already open.")
+ try:
+ self._port_handle = System.IO.Ports.SerialPort(self.portstr)
+ except Exception as msg:
+ self._port_handle = None
+ raise SerialException("could not open port %s: %s" % (self.portstr, msg))
+
+ # if RTS and/or DTR are not set before open, they default to True
+ if self._rts_state is None:
+ self._rts_state = True
+ if self._dtr_state is None:
+ self._dtr_state = True
+
+ self._reconfigure_port()
+ self._port_handle.Open()
+ self.is_open = True
+ if not self._dsrdtr:
+ self._update_dtr_state()
+ if not self._rtscts:
+ self._update_rts_state()
+ self.reset_input_buffer()
+
+ def _reconfigure_port(self):
+ """Set communication parameters on opened port."""
+ if not self._port_handle:
+ raise SerialException("Can only operate on a valid port handle")
+
+ #~ self._port_handle.ReceivedBytesThreshold = 1
+
+ if self._timeout is None:
+ self._port_handle.ReadTimeout = System.IO.Ports.SerialPort.InfiniteTimeout
+ else:
+ self._port_handle.ReadTimeout = int(self._timeout * 1000)
+
+ # if self._timeout != 0 and self._interCharTimeout is not None:
+ # timeouts = (int(self._interCharTimeout * 1000),) + timeouts[1:]
+
+ if self._write_timeout is None:
+ self._port_handle.WriteTimeout = System.IO.Ports.SerialPort.InfiniteTimeout
+ else:
+ self._port_handle.WriteTimeout = int(self._write_timeout * 1000)
+
+ # Setup the connection info.
+ try:
+ self._port_handle.BaudRate = self._baudrate
+ except IOError as e:
+ # catch errors from illegal baudrate settings
+ raise ValueError(str(e))
+
+ if self._bytesize == FIVEBITS:
+ self._port_handle.DataBits = 5
+ elif self._bytesize == SIXBITS:
+ self._port_handle.DataBits = 6
+ elif self._bytesize == SEVENBITS:
+ self._port_handle.DataBits = 7
+ elif self._bytesize == EIGHTBITS:
+ self._port_handle.DataBits = 8
+ else:
+ raise ValueError("Unsupported number of data bits: %r" % self._bytesize)
+
+ if self._parity == PARITY_NONE:
+ self._port_handle.Parity = getattr(System.IO.Ports.Parity, 'None') # reserved keyword in Py3k
+ elif self._parity == PARITY_EVEN:
+ self._port_handle.Parity = System.IO.Ports.Parity.Even
+ elif self._parity == PARITY_ODD:
+ self._port_handle.Parity = System.IO.Ports.Parity.Odd
+ elif self._parity == PARITY_MARK:
+ self._port_handle.Parity = System.IO.Ports.Parity.Mark
+ elif self._parity == PARITY_SPACE:
+ self._port_handle.Parity = System.IO.Ports.Parity.Space
+ else:
+ raise ValueError("Unsupported parity mode: %r" % self._parity)
+
+ if self._stopbits == STOPBITS_ONE:
+ self._port_handle.StopBits = System.IO.Ports.StopBits.One
+ elif self._stopbits == STOPBITS_ONE_POINT_FIVE:
+ self._port_handle.StopBits = System.IO.Ports.StopBits.OnePointFive
+ elif self._stopbits == STOPBITS_TWO:
+ self._port_handle.StopBits = System.IO.Ports.StopBits.Two
+ else:
+ raise ValueError("Unsupported number of stop bits: %r" % self._stopbits)
+
+ if self._rtscts and self._xonxoff:
+ self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSendXOnXOff
+ elif self._rtscts:
+ self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSend
+ elif self._xonxoff:
+ self._port_handle.Handshake = System.IO.Ports.Handshake.XOnXOff
+ else:
+ self._port_handle.Handshake = getattr(System.IO.Ports.Handshake, 'None') # reserved keyword in Py3k
+
+ #~ def __del__(self):
+ #~ self.close()
+
+ def close(self):
+ """Close port"""
+ if self.is_open:
+ if self._port_handle:
+ try:
+ self._port_handle.Close()
+ except System.IO.Ports.InvalidOperationException:
+ # ignore errors. can happen for unplugged USB serial devices
+ pass
+ self._port_handle = None
+ self.is_open = False
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+
+ @property
+ def in_waiting(self):
+ """Return the number of characters currently in the input buffer."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ return self._port_handle.BytesToRead
+
+ def read(self, size=1):
+ """\
+ Read size bytes from the serial port. If a timeout is set it may
+ return less characters as requested. With no timeout it will block
+ until the requested number of bytes is read.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ # must use single byte reads as this is the only way to read
+ # without applying encodings
+ data = bytearray()
+ while size:
+ try:
+ data.append(self._port_handle.ReadByte())
+ except System.TimeoutException:
+ break
+ else:
+ size -= 1
+ return bytes(data)
+
+ def write(self, data):
+ """Output the given string over the serial port."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ #~ if not isinstance(data, (bytes, bytearray)):
+ #~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
+ try:
+ # must call overloaded method with byte array argument
+ # as this is the only one not applying encodings
+ self._port_handle.Write(as_byte_array(data), 0, len(data))
+ except System.TimeoutException:
+ raise SerialTimeoutException('Write timeout')
+ return len(data)
+
+ def reset_input_buffer(self):
+ """Clear input buffer, discarding all that is in the buffer."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ self._port_handle.DiscardInBuffer()
+
+ def reset_output_buffer(self):
+ """\
+ Clear output buffer, aborting the current output and
+ discarding all that is in the buffer.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ self._port_handle.DiscardOutBuffer()
+
+ def _update_break_state(self):
+ """
+ Set break: Controls TXD. When active, to transmitting is possible.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ self._port_handle.BreakState = bool(self._break_state)
+
+ def _update_rts_state(self):
+ """Set terminal status line: Request To Send"""
+ if not self.is_open:
+ raise PortNotOpenError()
+ self._port_handle.RtsEnable = bool(self._rts_state)
+
+ def _update_dtr_state(self):
+ """Set terminal status line: Data Terminal Ready"""
+ if not self.is_open:
+ raise PortNotOpenError()
+ self._port_handle.DtrEnable = bool(self._dtr_state)
+
+ @property
+ def cts(self):
+ """Read terminal status line: Clear To Send"""
+ if not self.is_open:
+ raise PortNotOpenError()
+ return self._port_handle.CtsHolding
+
+ @property
+ def dsr(self):
+ """Read terminal status line: Data Set Ready"""
+ if not self.is_open:
+ raise PortNotOpenError()
+ return self._port_handle.DsrHolding
+
+ @property
+ def ri(self):
+ """Read terminal status line: Ring Indicator"""
+ if not self.is_open:
+ raise PortNotOpenError()
+ #~ return self._port_handle.XXX
+ return False # XXX an error would be better
+
+ @property
+ def cd(self):
+ """Read terminal status line: Carrier Detect"""
+ if not self.is_open:
+ raise PortNotOpenError()
+ return self._port_handle.CDHolding
+
+ # - - platform specific - - - -
+ # none
diff --git a/mixly/tools/python/serial/serialjava.py b/mixly/tools/python/serial/serialjava.py
new file mode 100644
index 00000000..0789a780
--- /dev/null
+++ b/mixly/tools/python/serial/serialjava.py
@@ -0,0 +1,251 @@
+#!jython
+#
+# Backend Jython with JavaComm
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2002-2015 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import absolute_import
+
+from serial.serialutil import *
+
+
+def my_import(name):
+ mod = __import__(name)
+ components = name.split('.')
+ for comp in components[1:]:
+ mod = getattr(mod, comp)
+ return mod
+
+
+def detect_java_comm(names):
+ """try given list of modules and return that imports"""
+ for name in names:
+ try:
+ mod = my_import(name)
+ mod.SerialPort
+ return mod
+ except (ImportError, AttributeError):
+ pass
+ raise ImportError("No Java Communications API implementation found")
+
+
+# Java Communications API implementations
+# http://mho.republika.pl/java/comm/
+
+comm = detect_java_comm([
+ 'javax.comm', # Sun/IBM
+ 'gnu.io', # RXTX
+])
+
+
+def device(portnumber):
+ """Turn a port number into a device name"""
+ enum = comm.CommPortIdentifier.getPortIdentifiers()
+ ports = []
+ while enum.hasMoreElements():
+ el = enum.nextElement()
+ if el.getPortType() == comm.CommPortIdentifier.PORT_SERIAL:
+ ports.append(el)
+ return ports[portnumber].getName()
+
+
+class Serial(SerialBase):
+ """\
+ Serial port class, implemented with Java Communications API and
+ thus usable with jython and the appropriate java extension.
+ """
+
+ def open(self):
+ """\
+ Open port with current settings. This may throw a SerialException
+ if the port cannot be opened.
+ """
+ if self._port is None:
+ raise SerialException("Port must be configured before it can be used.")
+ if self.is_open:
+ raise SerialException("Port is already open.")
+ if type(self._port) == type(''): # strings are taken directly
+ portId = comm.CommPortIdentifier.getPortIdentifier(self._port)
+ else:
+ portId = comm.CommPortIdentifier.getPortIdentifier(device(self._port)) # numbers are transformed to a comport id obj
+ try:
+ self.sPort = portId.open("python serial module", 10)
+ except Exception as msg:
+ self.sPort = None
+ raise SerialException("Could not open port: %s" % msg)
+ self._reconfigurePort()
+ self._instream = self.sPort.getInputStream()
+ self._outstream = self.sPort.getOutputStream()
+ self.is_open = True
+
+ def _reconfigurePort(self):
+ """Set communication parameters on opened port."""
+ if not self.sPort:
+ raise SerialException("Can only operate on a valid port handle")
+
+ self.sPort.enableReceiveTimeout(30)
+ if self._bytesize == FIVEBITS:
+ jdatabits = comm.SerialPort.DATABITS_5
+ elif self._bytesize == SIXBITS:
+ jdatabits = comm.SerialPort.DATABITS_6
+ elif self._bytesize == SEVENBITS:
+ jdatabits = comm.SerialPort.DATABITS_7
+ elif self._bytesize == EIGHTBITS:
+ jdatabits = comm.SerialPort.DATABITS_8
+ else:
+ raise ValueError("unsupported bytesize: %r" % self._bytesize)
+
+ if self._stopbits == STOPBITS_ONE:
+ jstopbits = comm.SerialPort.STOPBITS_1
+ elif self._stopbits == STOPBITS_ONE_POINT_FIVE:
+ jstopbits = comm.SerialPort.STOPBITS_1_5
+ elif self._stopbits == STOPBITS_TWO:
+ jstopbits = comm.SerialPort.STOPBITS_2
+ else:
+ raise ValueError("unsupported number of stopbits: %r" % self._stopbits)
+
+ if self._parity == PARITY_NONE:
+ jparity = comm.SerialPort.PARITY_NONE
+ elif self._parity == PARITY_EVEN:
+ jparity = comm.SerialPort.PARITY_EVEN
+ elif self._parity == PARITY_ODD:
+ jparity = comm.SerialPort.PARITY_ODD
+ elif self._parity == PARITY_MARK:
+ jparity = comm.SerialPort.PARITY_MARK
+ elif self._parity == PARITY_SPACE:
+ jparity = comm.SerialPort.PARITY_SPACE
+ else:
+ raise ValueError("unsupported parity type: %r" % self._parity)
+
+ jflowin = jflowout = 0
+ if self._rtscts:
+ jflowin |= comm.SerialPort.FLOWCONTROL_RTSCTS_IN
+ jflowout |= comm.SerialPort.FLOWCONTROL_RTSCTS_OUT
+ if self._xonxoff:
+ jflowin |= comm.SerialPort.FLOWCONTROL_XONXOFF_IN
+ jflowout |= comm.SerialPort.FLOWCONTROL_XONXOFF_OUT
+
+ self.sPort.setSerialPortParams(self._baudrate, jdatabits, jstopbits, jparity)
+ self.sPort.setFlowControlMode(jflowin | jflowout)
+
+ if self._timeout >= 0:
+ self.sPort.enableReceiveTimeout(int(self._timeout*1000))
+ else:
+ self.sPort.disableReceiveTimeout()
+
+ def close(self):
+ """Close port"""
+ if self.is_open:
+ if self.sPort:
+ self._instream.close()
+ self._outstream.close()
+ self.sPort.close()
+ self.sPort = None
+ self.is_open = False
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+
+ @property
+ def in_waiting(self):
+ """Return the number of characters currently in the input buffer."""
+ if not self.sPort:
+ raise PortNotOpenError()
+ return self._instream.available()
+
+ def read(self, size=1):
+ """\
+ Read size bytes from the serial port. If a timeout is set it may
+ return less characters as requested. With no timeout it will block
+ until the requested number of bytes is read.
+ """
+ if not self.sPort:
+ raise PortNotOpenError()
+ read = bytearray()
+ if size > 0:
+ while len(read) < size:
+ x = self._instream.read()
+ if x == -1:
+ if self.timeout >= 0:
+ break
+ else:
+ read.append(x)
+ return bytes(read)
+
+ def write(self, data):
+ """Output the given string over the serial port."""
+ if not self.sPort:
+ raise PortNotOpenError()
+ if not isinstance(data, (bytes, bytearray)):
+ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
+ self._outstream.write(data)
+ return len(data)
+
+ def reset_input_buffer(self):
+ """Clear input buffer, discarding all that is in the buffer."""
+ if not self.sPort:
+ raise PortNotOpenError()
+ self._instream.skip(self._instream.available())
+
+ def reset_output_buffer(self):
+ """\
+ Clear output buffer, aborting the current output and
+ discarding all that is in the buffer.
+ """
+ if not self.sPort:
+ raise PortNotOpenError()
+ self._outstream.flush()
+
+ def send_break(self, duration=0.25):
+ """Send break condition. Timed, returns to idle state after given duration."""
+ if not self.sPort:
+ raise PortNotOpenError()
+ self.sPort.sendBreak(duration*1000.0)
+
+ def _update_break_state(self):
+ """Set break: Controls TXD. When active, to transmitting is possible."""
+ if self.fd is None:
+ raise PortNotOpenError()
+ raise SerialException("The _update_break_state function is not implemented in java.")
+
+ def _update_rts_state(self):
+ """Set terminal status line: Request To Send"""
+ if not self.sPort:
+ raise PortNotOpenError()
+ self.sPort.setRTS(self._rts_state)
+
+ def _update_dtr_state(self):
+ """Set terminal status line: Data Terminal Ready"""
+ if not self.sPort:
+ raise PortNotOpenError()
+ self.sPort.setDTR(self._dtr_state)
+
+ @property
+ def cts(self):
+ """Read terminal status line: Clear To Send"""
+ if not self.sPort:
+ raise PortNotOpenError()
+ self.sPort.isCTS()
+
+ @property
+ def dsr(self):
+ """Read terminal status line: Data Set Ready"""
+ if not self.sPort:
+ raise PortNotOpenError()
+ self.sPort.isDSR()
+
+ @property
+ def ri(self):
+ """Read terminal status line: Ring Indicator"""
+ if not self.sPort:
+ raise PortNotOpenError()
+ self.sPort.isRI()
+
+ @property
+ def cd(self):
+ """Read terminal status line: Carrier Detect"""
+ if not self.sPort:
+ raise PortNotOpenError()
+ self.sPort.isCD()
diff --git a/mixly/tools/python/serial/serialposix.py b/mixly/tools/python/serial/serialposix.py
new file mode 100644
index 00000000..7aceb76d
--- /dev/null
+++ b/mixly/tools/python/serial/serialposix.py
@@ -0,0 +1,900 @@
+#!/usr/bin/env python
+#
+# backend for serial IO for POSIX compatible systems, like Linux, OSX
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2001-2020 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# parts based on code from Grant B. Edwards :
+# ftp://ftp.visi.com/users/grante/python/PosixSerial.py
+#
+# references: http://www.easysw.com/~mike/serial/serial.html
+
+# Collection of port names (was previously used by number_to_device which was
+# removed.
+# - Linux /dev/ttyS%d (confirmed)
+# - cygwin/win32 /dev/com%d (confirmed)
+# - openbsd (OpenBSD) /dev/cua%02d
+# - bsd*, freebsd* /dev/cuad%d
+# - darwin (OS X) /dev/cuad%d
+# - netbsd /dev/dty%02d (NetBSD 1.6 testing by Erk)
+# - irix (IRIX) /dev/ttyf%d (partially tested) names depending on flow control
+# - hp (HP-UX) /dev/tty%dp0 (not tested)
+# - sunos (Solaris/SunOS) /dev/tty%c (letters, 'a'..'z') (confirmed)
+# - aix (AIX) /dev/tty%d
+
+
+from __future__ import absolute_import
+
+# pylint: disable=abstract-method
+import errno
+import fcntl
+import os
+import select
+import struct
+import sys
+import termios
+
+import serial
+from serial.serialutil import SerialBase, SerialException, to_bytes, \
+ PortNotOpenError, SerialTimeoutException, Timeout
+
+
+class PlatformSpecificBase(object):
+ BAUDRATE_CONSTANTS = {}
+
+ def _set_special_baudrate(self, baudrate):
+ raise NotImplementedError('non-standard baudrates are not supported on this platform')
+
+ def _set_rs485_mode(self, rs485_settings):
+ raise NotImplementedError('RS485 not supported on this platform')
+
+ def set_low_latency_mode(self, low_latency_settings):
+ raise NotImplementedError('Low latency not supported on this platform')
+
+ def _update_break_state(self):
+ """\
+ Set break: Controls TXD. When active, no transmitting is possible.
+ """
+ if self._break_state:
+ fcntl.ioctl(self.fd, TIOCSBRK)
+ else:
+ fcntl.ioctl(self.fd, TIOCCBRK)
+
+
+# some systems support an extra flag to enable the two in POSIX unsupported
+# paritiy settings for MARK and SPACE
+CMSPAR = 0 # default, for unsupported platforms, override below
+
+# try to detect the OS so that a device can be selected...
+# this code block should supply a device() and set_special_baudrate() function
+# for the platform
+plat = sys.platform.lower()
+
+if plat[:5] == 'linux': # Linux (confirmed) # noqa
+ import array
+
+ # extra termios flags
+ CMSPAR = 0o10000000000 # Use "stick" (mark/space) parity
+
+ # baudrate ioctls
+ TCGETS2 = 0x802C542A
+ TCSETS2 = 0x402C542B
+ BOTHER = 0o010000
+
+ # RS485 ioctls
+ TIOCGRS485 = 0x542E
+ TIOCSRS485 = 0x542F
+ SER_RS485_ENABLED = 0b00000001
+ SER_RS485_RTS_ON_SEND = 0b00000010
+ SER_RS485_RTS_AFTER_SEND = 0b00000100
+ SER_RS485_RX_DURING_TX = 0b00010000
+
+ class PlatformSpecific(PlatformSpecificBase):
+ BAUDRATE_CONSTANTS = {
+ 0: 0o000000, # hang up
+ 50: 0o000001,
+ 75: 0o000002,
+ 110: 0o000003,
+ 134: 0o000004,
+ 150: 0o000005,
+ 200: 0o000006,
+ 300: 0o000007,
+ 600: 0o000010,
+ 1200: 0o000011,
+ 1800: 0o000012,
+ 2400: 0o000013,
+ 4800: 0o000014,
+ 9600: 0o000015,
+ 19200: 0o000016,
+ 38400: 0o000017,
+ 57600: 0o010001,
+ 115200: 0o010002,
+ 230400: 0o010003,
+ 460800: 0o010004,
+ 500000: 0o010005,
+ 576000: 0o010006,
+ 921600: 0o010007,
+ 1000000: 0o010010,
+ 1152000: 0o010011,
+ 1500000: 0o010012,
+ 2000000: 0o010013,
+ 2500000: 0o010014,
+ 3000000: 0o010015,
+ 3500000: 0o010016,
+ 4000000: 0o010017
+ }
+
+ def set_low_latency_mode(self, low_latency_settings):
+ buf = array.array('i', [0] * 32)
+
+ try:
+ # get serial_struct
+ fcntl.ioctl(self.fd, termios.TIOCGSERIAL, buf)
+
+ # set or unset ASYNC_LOW_LATENCY flag
+ if low_latency_settings:
+ buf[4] |= 0x2000
+ else:
+ buf[4] &= ~0x2000
+
+ # set serial_struct
+ fcntl.ioctl(self.fd, termios.TIOCSSERIAL, buf)
+ except IOError as e:
+ raise ValueError('Failed to update ASYNC_LOW_LATENCY flag to {}: {}'.format(low_latency_settings, e))
+
+ def _set_special_baudrate(self, baudrate):
+ # right size is 44 on x86_64, allow for some growth
+ buf = array.array('i', [0] * 64)
+ try:
+ # get serial_struct
+ fcntl.ioctl(self.fd, TCGETS2, buf)
+ # set custom speed
+ buf[2] &= ~termios.CBAUD
+ buf[2] |= BOTHER
+ buf[9] = buf[10] = baudrate
+
+ # set serial_struct
+ fcntl.ioctl(self.fd, TCSETS2, buf)
+ except IOError as e:
+ raise ValueError('Failed to set custom baud rate ({}): {}'.format(baudrate, e))
+
+ def _set_rs485_mode(self, rs485_settings):
+ buf = array.array('i', [0] * 8) # flags, delaytx, delayrx, padding
+ try:
+ fcntl.ioctl(self.fd, TIOCGRS485, buf)
+ buf[0] |= SER_RS485_ENABLED
+ if rs485_settings is not None:
+ if rs485_settings.loopback:
+ buf[0] |= SER_RS485_RX_DURING_TX
+ else:
+ buf[0] &= ~SER_RS485_RX_DURING_TX
+ if rs485_settings.rts_level_for_tx:
+ buf[0] |= SER_RS485_RTS_ON_SEND
+ else:
+ buf[0] &= ~SER_RS485_RTS_ON_SEND
+ if rs485_settings.rts_level_for_rx:
+ buf[0] |= SER_RS485_RTS_AFTER_SEND
+ else:
+ buf[0] &= ~SER_RS485_RTS_AFTER_SEND
+ if rs485_settings.delay_before_tx is not None:
+ buf[1] = int(rs485_settings.delay_before_tx * 1000)
+ if rs485_settings.delay_before_rx is not None:
+ buf[2] = int(rs485_settings.delay_before_rx * 1000)
+ else:
+ buf[0] = 0 # clear SER_RS485_ENABLED
+ fcntl.ioctl(self.fd, TIOCSRS485, buf)
+ except IOError as e:
+ raise ValueError('Failed to set RS485 mode: {}'.format(e))
+
+
+elif plat == 'cygwin': # cygwin/win32 (confirmed)
+
+ class PlatformSpecific(PlatformSpecificBase):
+ BAUDRATE_CONSTANTS = {
+ 128000: 0x01003,
+ 256000: 0x01005,
+ 500000: 0x01007,
+ 576000: 0x01008,
+ 921600: 0x01009,
+ 1000000: 0x0100a,
+ 1152000: 0x0100b,
+ 1500000: 0x0100c,
+ 2000000: 0x0100d,
+ 2500000: 0x0100e,
+ 3000000: 0x0100f
+ }
+
+
+elif plat[:6] == 'darwin': # OS X
+ import array
+ IOSSIOSPEED = 0x80045402 # _IOW('T', 2, speed_t)
+
+ class PlatformSpecific(PlatformSpecificBase):
+ osx_version = os.uname()[2].split('.')
+ TIOCSBRK = 0x2000747B # _IO('t', 123)
+ TIOCCBRK = 0x2000747A # _IO('t', 122)
+
+ # Tiger or above can support arbitrary serial speeds
+ if int(osx_version[0]) >= 8:
+ def _set_special_baudrate(self, baudrate):
+ # use IOKit-specific call to set up high speeds
+ buf = array.array('i', [baudrate])
+ fcntl.ioctl(self.fd, IOSSIOSPEED, buf, 1)
+
+ def _update_break_state(self):
+ """\
+ Set break: Controls TXD. When active, no transmitting is possible.
+ """
+ if self._break_state:
+ fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
+ else:
+ fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)
+
+elif plat[:3] == 'bsd' or \
+ plat[:7] == 'freebsd' or \
+ plat[:6] == 'netbsd' or \
+ plat[:7] == 'openbsd':
+
+ class ReturnBaudrate(object):
+ def __getitem__(self, key):
+ return key
+
+ class PlatformSpecific(PlatformSpecificBase):
+ # Only tested on FreeBSD:
+ # The baud rate may be passed in as
+ # a literal value.
+ BAUDRATE_CONSTANTS = ReturnBaudrate()
+
+ TIOCSBRK = 0x2000747B # _IO('t', 123)
+ TIOCCBRK = 0x2000747A # _IO('t', 122)
+
+
+ def _update_break_state(self):
+ """\
+ Set break: Controls TXD. When active, no transmitting is possible.
+ """
+ if self._break_state:
+ fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
+ else:
+ fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)
+
+else:
+ class PlatformSpecific(PlatformSpecificBase):
+ pass
+
+
+# load some constants for later use.
+# try to use values from termios, use defaults from linux otherwise
+TIOCMGET = getattr(termios, 'TIOCMGET', 0x5415)
+TIOCMBIS = getattr(termios, 'TIOCMBIS', 0x5416)
+TIOCMBIC = getattr(termios, 'TIOCMBIC', 0x5417)
+TIOCMSET = getattr(termios, 'TIOCMSET', 0x5418)
+
+# TIOCM_LE = getattr(termios, 'TIOCM_LE', 0x001)
+TIOCM_DTR = getattr(termios, 'TIOCM_DTR', 0x002)
+TIOCM_RTS = getattr(termios, 'TIOCM_RTS', 0x004)
+# TIOCM_ST = getattr(termios, 'TIOCM_ST', 0x008)
+# TIOCM_SR = getattr(termios, 'TIOCM_SR', 0x010)
+
+TIOCM_CTS = getattr(termios, 'TIOCM_CTS', 0x020)
+TIOCM_CAR = getattr(termios, 'TIOCM_CAR', 0x040)
+TIOCM_RNG = getattr(termios, 'TIOCM_RNG', 0x080)
+TIOCM_DSR = getattr(termios, 'TIOCM_DSR', 0x100)
+TIOCM_CD = getattr(termios, 'TIOCM_CD', TIOCM_CAR)
+TIOCM_RI = getattr(termios, 'TIOCM_RI', TIOCM_RNG)
+# TIOCM_OUT1 = getattr(termios, 'TIOCM_OUT1', 0x2000)
+# TIOCM_OUT2 = getattr(termios, 'TIOCM_OUT2', 0x4000)
+if hasattr(termios, 'TIOCINQ'):
+ TIOCINQ = termios.TIOCINQ
+else:
+ TIOCINQ = getattr(termios, 'FIONREAD', 0x541B)
+TIOCOUTQ = getattr(termios, 'TIOCOUTQ', 0x5411)
+
+TIOCM_zero_str = struct.pack('I', 0)
+TIOCM_RTS_str = struct.pack('I', TIOCM_RTS)
+TIOCM_DTR_str = struct.pack('I', TIOCM_DTR)
+
+TIOCSBRK = getattr(termios, 'TIOCSBRK', 0x5427)
+TIOCCBRK = getattr(termios, 'TIOCCBRK', 0x5428)
+
+
+class Serial(SerialBase, PlatformSpecific):
+ """\
+ Serial port class POSIX implementation. Serial port configuration is
+ done with termios and fcntl. Runs on Linux and many other Un*x like
+ systems.
+ """
+
+ def open(self):
+ """\
+ Open port with current settings. This may throw a SerialException
+ if the port cannot be opened."""
+ if self._port is None:
+ raise SerialException("Port must be configured before it can be used.")
+ if self.is_open:
+ raise SerialException("Port is already open.")
+ self.fd = None
+ # open
+ try:
+ self.fd = os.open(self.portstr, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
+ except OSError as msg:
+ self.fd = None
+ raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg))
+ #~ fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # set blocking
+
+ self.pipe_abort_read_r, self.pipe_abort_read_w = None, None
+ self.pipe_abort_write_r, self.pipe_abort_write_w = None, None
+
+ try:
+ self._reconfigure_port(force_update=True)
+
+ try:
+ if not self._dsrdtr:
+ self._update_dtr_state()
+ if not self._rtscts:
+ self._update_rts_state()
+ except IOError as e:
+ # ignore Invalid argument and Inappropriate ioctl
+ if e.errno not in (errno.EINVAL, errno.ENOTTY):
+ raise
+
+ self._reset_input_buffer()
+
+ self.pipe_abort_read_r, self.pipe_abort_read_w = os.pipe()
+ self.pipe_abort_write_r, self.pipe_abort_write_w = os.pipe()
+ fcntl.fcntl(self.pipe_abort_read_r, fcntl.F_SETFL, os.O_NONBLOCK)
+ fcntl.fcntl(self.pipe_abort_write_r, fcntl.F_SETFL, os.O_NONBLOCK)
+ except BaseException:
+ try:
+ os.close(self.fd)
+ except Exception:
+ # ignore any exception when closing the port
+ # also to keep original exception that happened when setting up
+ pass
+ self.fd = None
+
+ if self.pipe_abort_read_w is not None:
+ os.close(self.pipe_abort_read_w)
+ self.pipe_abort_read_w = None
+ if self.pipe_abort_read_r is not None:
+ os.close(self.pipe_abort_read_r)
+ self.pipe_abort_read_r = None
+ if self.pipe_abort_write_w is not None:
+ os.close(self.pipe_abort_write_w)
+ self.pipe_abort_write_w = None
+ if self.pipe_abort_write_r is not None:
+ os.close(self.pipe_abort_write_r)
+ self.pipe_abort_write_r = None
+
+ raise
+
+ self.is_open = True
+
+ def _reconfigure_port(self, force_update=False):
+ """Set communication parameters on opened port."""
+ if self.fd is None:
+ raise SerialException("Can only operate on a valid file descriptor")
+
+ # if exclusive lock is requested, create it before we modify anything else
+ if self._exclusive is not None:
+ if self._exclusive:
+ try:
+ fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except IOError as msg:
+ raise SerialException(msg.errno, "Could not exclusively lock port {}: {}".format(self._port, msg))
+ else:
+ fcntl.flock(self.fd, fcntl.LOCK_UN)
+
+ custom_baud = None
+
+ vmin = vtime = 0 # timeout is done via select
+ if self._inter_byte_timeout is not None:
+ vmin = 1
+ vtime = int(self._inter_byte_timeout * 10)
+ try:
+ orig_attr = termios.tcgetattr(self.fd)
+ iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr
+ except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here
+ raise SerialException("Could not configure port: {}".format(msg))
+ # set up raw mode / no echo / binary
+ cflag |= (termios.CLOCAL | termios.CREAD)
+ lflag &= ~(termios.ICANON | termios.ECHO | termios.ECHOE |
+ termios.ECHOK | termios.ECHONL |
+ termios.ISIG | termios.IEXTEN) # |termios.ECHOPRT
+ for flag in ('ECHOCTL', 'ECHOKE'): # netbsd workaround for Erk
+ if hasattr(termios, flag):
+ lflag &= ~getattr(termios, flag)
+
+ oflag &= ~(termios.OPOST | termios.ONLCR | termios.OCRNL)
+ iflag &= ~(termios.INLCR | termios.IGNCR | termios.ICRNL | termios.IGNBRK)
+ if hasattr(termios, 'IUCLC'):
+ iflag &= ~termios.IUCLC
+ if hasattr(termios, 'PARMRK'):
+ iflag &= ~termios.PARMRK
+
+ # setup baud rate
+ try:
+ ispeed = ospeed = getattr(termios, 'B{}'.format(self._baudrate))
+ except AttributeError:
+ try:
+ ispeed = ospeed = self.BAUDRATE_CONSTANTS[self._baudrate]
+ except KeyError:
+ #~ raise ValueError('Invalid baud rate: %r' % self._baudrate)
+
+ # See if BOTHER is defined for this platform; if it is, use
+ # this for a speed not defined in the baudrate constants list.
+ try:
+ ispeed = ospeed = BOTHER
+ except NameError:
+ # may need custom baud rate, it isn't in our list.
+ ispeed = ospeed = getattr(termios, 'B38400')
+
+ try:
+ custom_baud = int(self._baudrate) # store for later
+ except ValueError:
+ raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate))
+ else:
+ if custom_baud < 0:
+ raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate))
+
+ # setup char len
+ cflag &= ~termios.CSIZE
+ if self._bytesize == 8:
+ cflag |= termios.CS8
+ elif self._bytesize == 7:
+ cflag |= termios.CS7
+ elif self._bytesize == 6:
+ cflag |= termios.CS6
+ elif self._bytesize == 5:
+ cflag |= termios.CS5
+ else:
+ raise ValueError('Invalid char len: {!r}'.format(self._bytesize))
+ # setup stop bits
+ if self._stopbits == serial.STOPBITS_ONE:
+ cflag &= ~(termios.CSTOPB)
+ elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
+ cflag |= (termios.CSTOPB) # XXX same as TWO.. there is no POSIX support for 1.5
+ elif self._stopbits == serial.STOPBITS_TWO:
+ cflag |= (termios.CSTOPB)
+ else:
+ raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits))
+ # setup parity
+ iflag &= ~(termios.INPCK | termios.ISTRIP)
+ if self._parity == serial.PARITY_NONE:
+ cflag &= ~(termios.PARENB | termios.PARODD | CMSPAR)
+ elif self._parity == serial.PARITY_EVEN:
+ cflag &= ~(termios.PARODD | CMSPAR)
+ cflag |= (termios.PARENB)
+ elif self._parity == serial.PARITY_ODD:
+ cflag &= ~CMSPAR
+ cflag |= (termios.PARENB | termios.PARODD)
+ elif self._parity == serial.PARITY_MARK and CMSPAR:
+ cflag |= (termios.PARENB | CMSPAR | termios.PARODD)
+ elif self._parity == serial.PARITY_SPACE and CMSPAR:
+ cflag |= (termios.PARENB | CMSPAR)
+ cflag &= ~(termios.PARODD)
+ else:
+ raise ValueError('Invalid parity: {!r}'.format(self._parity))
+ # setup flow control
+ # xonxoff
+ if hasattr(termios, 'IXANY'):
+ if self._xonxoff:
+ iflag |= (termios.IXON | termios.IXOFF) # |termios.IXANY)
+ else:
+ iflag &= ~(termios.IXON | termios.IXOFF | termios.IXANY)
+ else:
+ if self._xonxoff:
+ iflag |= (termios.IXON | termios.IXOFF)
+ else:
+ iflag &= ~(termios.IXON | termios.IXOFF)
+ # rtscts
+ if hasattr(termios, 'CRTSCTS'):
+ if self._rtscts:
+ cflag |= (termios.CRTSCTS)
+ else:
+ cflag &= ~(termios.CRTSCTS)
+ elif hasattr(termios, 'CNEW_RTSCTS'): # try it with alternate constant name
+ if self._rtscts:
+ cflag |= (termios.CNEW_RTSCTS)
+ else:
+ cflag &= ~(termios.CNEW_RTSCTS)
+ # XXX should there be a warning if setting up rtscts (and xonxoff etc) fails??
+
+ # buffer
+ # vmin "minimal number of characters to be read. 0 for non blocking"
+ if vmin < 0 or vmin > 255:
+ raise ValueError('Invalid vmin: {!r}'.format(vmin))
+ cc[termios.VMIN] = vmin
+ # vtime
+ if vtime < 0 or vtime > 255:
+ raise ValueError('Invalid vtime: {!r}'.format(vtime))
+ cc[termios.VTIME] = vtime
+ # activate settings
+ if force_update or [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] != orig_attr:
+ termios.tcsetattr(
+ self.fd,
+ termios.TCSANOW,
+ [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
+
+ # apply custom baud rate, if any
+ if custom_baud is not None:
+ self._set_special_baudrate(custom_baud)
+
+ if self._rs485_mode is not None:
+ self._set_rs485_mode(self._rs485_mode)
+
+ def close(self):
+ """Close port"""
+ if self.is_open:
+ if self.fd is not None:
+ os.close(self.fd)
+ self.fd = None
+ os.close(self.pipe_abort_read_w)
+ os.close(self.pipe_abort_read_r)
+ os.close(self.pipe_abort_write_w)
+ os.close(self.pipe_abort_write_r)
+ self.pipe_abort_read_r, self.pipe_abort_read_w = None, None
+ self.pipe_abort_write_r, self.pipe_abort_write_w = None, None
+ self.is_open = False
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+
+ @property
+ def in_waiting(self):
+ """Return the number of bytes currently in the input buffer."""
+ #~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str)
+ s = fcntl.ioctl(self.fd, TIOCINQ, TIOCM_zero_str)
+ return struct.unpack('I', s)[0]
+
+ # select based implementation, proved to work on many systems
+ def read(self, size=1):
+ """\
+ Read size bytes from the serial port. If a timeout is set it may
+ return less characters as requested. With no timeout it will block
+ until the requested number of bytes is read.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ read = bytearray()
+ timeout = Timeout(self._timeout)
+ while len(read) < size:
+ try:
+ ready, _, _ = select.select([self.fd, self.pipe_abort_read_r], [], [], timeout.time_left())
+ if self.pipe_abort_read_r in ready:
+ os.read(self.pipe_abort_read_r, 1000)
+ break
+ # If select was used with a timeout, and the timeout occurs, it
+ # returns with empty lists -> thus abort read operation.
+ # For timeout == 0 (non-blocking operation) also abort when
+ # there is nothing to read.
+ if not ready:
+ break # timeout
+ buf = os.read(self.fd, size - len(read))
+ except OSError as e:
+ # this is for Python 3.x where select.error is a subclass of
+ # OSError ignore BlockingIOErrors and EINTR. other errors are shown
+ # https://www.python.org/dev/peps/pep-0475.
+ if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
+ raise SerialException('read failed: {}'.format(e))
+ except select.error as e:
+ # this is for Python 2.x
+ # ignore BlockingIOErrors and EINTR. all errors are shown
+ # see also http://www.python.org/dev/peps/pep-3151/#select
+ if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
+ raise SerialException('read failed: {}'.format(e))
+ else:
+ # read should always return some data as select reported it was
+ # ready to read when we get to this point.
+ if not buf:
+ # Disconnected devices, at least on Linux, show the
+ # behavior that they are always ready to read immediately
+ # but reading returns nothing.
+ raise SerialException(
+ 'device reports readiness to read but returned no data '
+ '(device disconnected or multiple access on port?)')
+ read.extend(buf)
+
+ if timeout.expired():
+ break
+ return bytes(read)
+
+ def cancel_read(self):
+ if self.is_open:
+ os.write(self.pipe_abort_read_w, b"x")
+
+ def cancel_write(self):
+ if self.is_open:
+ os.write(self.pipe_abort_write_w, b"x")
+
+ def write(self, data):
+ """Output the given byte string over the serial port."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ d = to_bytes(data)
+ tx_len = length = len(d)
+ timeout = Timeout(self._write_timeout)
+ while tx_len > 0:
+ try:
+ n = os.write(self.fd, d)
+ if timeout.is_non_blocking:
+ # Zero timeout indicates non-blocking - simply return the
+ # number of bytes of data actually written
+ return n
+ elif not timeout.is_infinite:
+ # when timeout is set, use select to wait for being ready
+ # with the time left as timeout
+ if timeout.expired():
+ raise SerialTimeoutException('Write timeout')
+ abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], timeout.time_left())
+ if abort:
+ os.read(self.pipe_abort_write_r, 1000)
+ break
+ if not ready:
+ raise SerialTimeoutException('Write timeout')
+ else:
+ assert timeout.time_left() is None
+ # wait for write operation
+ abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], None)
+ if abort:
+ os.read(self.pipe_abort_write_r, 1)
+ break
+ if not ready:
+ raise SerialException('write failed (select)')
+ d = d[n:]
+ tx_len -= n
+ except SerialException:
+ raise
+ except OSError as e:
+ # this is for Python 3.x where select.error is a subclass of
+ # OSError ignore BlockingIOErrors and EINTR. other errors are shown
+ # https://www.python.org/dev/peps/pep-0475.
+ if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
+ raise SerialException('write failed: {}'.format(e))
+ except select.error as e:
+ # this is for Python 2.x
+ # ignore BlockingIOErrors and EINTR. all errors are shown
+ # see also http://www.python.org/dev/peps/pep-3151/#select
+ if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
+ raise SerialException('write failed: {}'.format(e))
+ if not timeout.is_non_blocking and timeout.expired():
+ raise SerialTimeoutException('Write timeout')
+ return length - len(d)
+
+ def flush(self):
+ """\
+ Flush of file like objects. In this case, wait until all data
+ is written.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ termios.tcdrain(self.fd)
+
+ def _reset_input_buffer(self):
+ """Clear input buffer, discarding all that is in the buffer."""
+ termios.tcflush(self.fd, termios.TCIFLUSH)
+
+ def reset_input_buffer(self):
+ """Clear input buffer, discarding all that is in the buffer."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ self._reset_input_buffer()
+
+ def reset_output_buffer(self):
+ """\
+ Clear output buffer, aborting the current output and discarding all
+ that is in the buffer.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ termios.tcflush(self.fd, termios.TCOFLUSH)
+
+ def send_break(self, duration=0.25):
+ """\
+ Send break condition. Timed, returns to idle state after given
+ duration.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ termios.tcsendbreak(self.fd, int(duration / 0.25))
+
+ def _update_rts_state(self):
+ """Set terminal status line: Request To Send"""
+ if self._rts_state:
+ fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_RTS_str)
+ else:
+ fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_RTS_str)
+
+ def _update_dtr_state(self):
+ """Set terminal status line: Data Terminal Ready"""
+ if self._dtr_state:
+ fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_DTR_str)
+ else:
+ fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_DTR_str)
+
+ @property
+ def cts(self):
+ """Read terminal status line: Clear To Send"""
+ if not self.is_open:
+ raise PortNotOpenError()
+ s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
+ return struct.unpack('I', s)[0] & TIOCM_CTS != 0
+
+ @property
+ def dsr(self):
+ """Read terminal status line: Data Set Ready"""
+ if not self.is_open:
+ raise PortNotOpenError()
+ s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
+ return struct.unpack('I', s)[0] & TIOCM_DSR != 0
+
+ @property
+ def ri(self):
+ """Read terminal status line: Ring Indicator"""
+ if not self.is_open:
+ raise PortNotOpenError()
+ s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
+ return struct.unpack('I', s)[0] & TIOCM_RI != 0
+
+ @property
+ def cd(self):
+ """Read terminal status line: Carrier Detect"""
+ if not self.is_open:
+ raise PortNotOpenError()
+ s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
+ return struct.unpack('I', s)[0] & TIOCM_CD != 0
+
+ # - - platform specific - - - -
+
+ @property
+ def out_waiting(self):
+ """Return the number of bytes currently in the output buffer."""
+ #~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str)
+ s = fcntl.ioctl(self.fd, TIOCOUTQ, TIOCM_zero_str)
+ return struct.unpack('I', s)[0]
+
+ def fileno(self):
+ """\
+ For easier use of the serial port instance with select.
+ WARNING: this function is not portable to different platforms!
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ return self.fd
+
+ def set_input_flow_control(self, enable=True):
+ """\
+ Manually control flow - when software flow control is enabled.
+ This will send XON (true) or XOFF (false) to the other device.
+ WARNING: this function is not portable to different platforms!
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ if enable:
+ termios.tcflow(self.fd, termios.TCION)
+ else:
+ termios.tcflow(self.fd, termios.TCIOFF)
+
+ def set_output_flow_control(self, enable=True):
+ """\
+ Manually control flow of outgoing data - when hardware or software flow
+ control is enabled.
+ WARNING: this function is not portable to different platforms!
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ if enable:
+ termios.tcflow(self.fd, termios.TCOON)
+ else:
+ termios.tcflow(self.fd, termios.TCOOFF)
+
+ def nonblocking(self):
+ """DEPRECATED - has no use"""
+ import warnings
+ warnings.warn("nonblocking() has no effect, already nonblocking", DeprecationWarning)
+
+
+class PosixPollSerial(Serial):
+ """\
+ Poll based read implementation. Not all systems support poll properly.
+ However this one has better handling of errors, such as a device
+ disconnecting while it's in use (e.g. USB-serial unplugged).
+ """
+
+ def read(self, size=1):
+ """\
+ Read size bytes from the serial port. If a timeout is set it may
+ return less characters as requested. With no timeout it will block
+ until the requested number of bytes is read.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ read = bytearray()
+ timeout = Timeout(self._timeout)
+ poll = select.poll()
+ poll.register(self.fd, select.POLLIN | select.POLLERR | select.POLLHUP | select.POLLNVAL)
+ poll.register(self.pipe_abort_read_r, select.POLLIN | select.POLLERR | select.POLLHUP | select.POLLNVAL)
+ if size > 0:
+ while len(read) < size:
+ # print "\tread(): size",size, "have", len(read) #debug
+ # wait until device becomes ready to read (or something fails)
+ for fd, event in poll.poll(None if timeout.is_infinite else (timeout.time_left() * 1000)):
+ if fd == self.pipe_abort_read_r:
+ break
+ if event & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
+ raise SerialException('device reports error (poll)')
+ # we don't care if it is select.POLLIN or timeout, that's
+ # handled below
+ if fd == self.pipe_abort_read_r:
+ os.read(self.pipe_abort_read_r, 1000)
+ break
+ buf = os.read(self.fd, size - len(read))
+ read.extend(buf)
+ if timeout.expired() \
+ or (self._inter_byte_timeout is not None and self._inter_byte_timeout > 0) and not buf:
+ break # early abort on timeout
+ return bytes(read)
+
+
+class VTIMESerial(Serial):
+ """\
+ Implement timeout using vtime of tty device instead of using select.
+ This means that no inter character timeout can be specified and that
+ the error handling is degraded.
+
+ Overall timeout is disabled when inter-character timeout is used.
+
+ Note that this implementation does NOT support cancel_read(), it will
+ just ignore that.
+ """
+
+ def _reconfigure_port(self, force_update=True):
+ """Set communication parameters on opened port."""
+ super(VTIMESerial, self)._reconfigure_port()
+ fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # clear O_NONBLOCK
+
+ if self._inter_byte_timeout is not None:
+ vmin = 1
+ vtime = int(self._inter_byte_timeout * 10)
+ elif self._timeout is None:
+ vmin = 1
+ vtime = 0
+ else:
+ vmin = 0
+ vtime = int(self._timeout * 10)
+ try:
+ orig_attr = termios.tcgetattr(self.fd)
+ iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr
+ except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here
+ raise serial.SerialException("Could not configure port: {}".format(msg))
+
+ if vtime < 0 or vtime > 255:
+ raise ValueError('Invalid vtime: {!r}'.format(vtime))
+ cc[termios.VTIME] = vtime
+ cc[termios.VMIN] = vmin
+
+ termios.tcsetattr(
+ self.fd,
+ termios.TCSANOW,
+ [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
+
+ def read(self, size=1):
+ """\
+ Read size bytes from the serial port. If a timeout is set it may
+ return less characters as requested. With no timeout it will block
+ until the requested number of bytes is read.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ read = bytearray()
+ while len(read) < size:
+ buf = os.read(self.fd, size - len(read))
+ if not buf:
+ break
+ read.extend(buf)
+ return bytes(read)
+
+ # hack to make hasattr return false
+ cancel_read = property()
diff --git a/mixly/tools/python/serial/serialutil.py b/mixly/tools/python/serial/serialutil.py
new file mode 100644
index 00000000..789219e9
--- /dev/null
+++ b/mixly/tools/python/serial/serialutil.py
@@ -0,0 +1,697 @@
+#! python
+#
+# Base class and support functions used by various backends.
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2001-2020 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import absolute_import
+
+import io
+import time
+
+# ``memoryview`` was introduced in Python 2.7 and ``bytes(some_memoryview)``
+# isn't returning the contents (very unfortunate). Therefore we need special
+# cases and test for it. Ensure that there is a ``memoryview`` object for older
+# Python versions. This is easier than making every test dependent on its
+# existence.
+try:
+ memoryview
+except (NameError, AttributeError):
+ # implementation does not matter as we do not really use it.
+ # it just must not inherit from something else we might care for.
+ class memoryview(object): # pylint: disable=redefined-builtin,invalid-name
+ pass
+
+try:
+ unicode
+except (NameError, AttributeError):
+ unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name
+
+try:
+ basestring
+except (NameError, AttributeError):
+ basestring = (str,) # for Python 3, pylint: disable=redefined-builtin,invalid-name
+
+
+# "for byte in data" fails for python3 as it returns ints instead of bytes
+def iterbytes(b):
+ """Iterate over bytes, returning bytes instead of ints (python3)"""
+ if isinstance(b, memoryview):
+ b = b.tobytes()
+ i = 0
+ while True:
+ a = b[i:i + 1]
+ i += 1
+ if a:
+ yield a
+ else:
+ break
+
+
+# all Python versions prior 3.x convert ``str([17])`` to '[17]' instead of '\x11'
+# so a simple ``bytes(sequence)`` doesn't work for all versions
+def to_bytes(seq):
+ """convert a sequence to a bytes type"""
+ if isinstance(seq, bytes):
+ return seq
+ elif isinstance(seq, bytearray):
+ return bytes(seq)
+ elif isinstance(seq, memoryview):
+ return seq.tobytes()
+ elif isinstance(seq, unicode):
+ raise TypeError('unicode strings are not supported, please encode to bytes: {!r}'.format(seq))
+ else:
+ # handle list of integers and bytes (one or more items) for Python 2 and 3
+ return bytes(bytearray(seq))
+
+
+# create control bytes
+XON = to_bytes([17])
+XOFF = to_bytes([19])
+
+CR = to_bytes([13])
+LF = to_bytes([10])
+
+
+PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE = 'N', 'E', 'O', 'M', 'S'
+STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2)
+FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS = (5, 6, 7, 8)
+
+PARITY_NAMES = {
+ PARITY_NONE: 'None',
+ PARITY_EVEN: 'Even',
+ PARITY_ODD: 'Odd',
+ PARITY_MARK: 'Mark',
+ PARITY_SPACE: 'Space',
+}
+
+
+class SerialException(IOError):
+ """Base class for serial port related exceptions."""
+
+
+class SerialTimeoutException(SerialException):
+ """Write timeouts give an exception"""
+
+
+class PortNotOpenError(SerialException):
+ """Port is not open"""
+ def __init__(self):
+ super(PortNotOpenError, self).__init__('Attempting to use a port that is not open')
+
+
+class Timeout(object):
+ """\
+ Abstraction for timeout operations. Using time.monotonic() if available
+ or time.time() in all other cases.
+
+ The class can also be initialized with 0 or None, in order to support
+ non-blocking and fully blocking I/O operations. The attributes
+ is_non_blocking and is_infinite are set accordingly.
+ """
+ if hasattr(time, 'monotonic'):
+ # Timeout implementation with time.monotonic(). This function is only
+ # supported by Python 3.3 and above. It returns a time in seconds
+ # (float) just as time.time(), but is not affected by system clock
+ # adjustments.
+ TIME = time.monotonic
+ else:
+ # Timeout implementation with time.time(). This is compatible with all
+ # Python versions but has issues if the clock is adjusted while the
+ # timeout is running.
+ TIME = time.time
+
+ def __init__(self, duration):
+ """Initialize a timeout with given duration"""
+ self.is_infinite = (duration is None)
+ self.is_non_blocking = (duration == 0)
+ self.duration = duration
+ if duration is not None:
+ self.target_time = self.TIME() + duration
+ else:
+ self.target_time = None
+
+ def expired(self):
+ """Return a boolean, telling if the timeout has expired"""
+ return self.target_time is not None and self.time_left() <= 0
+
+ def time_left(self):
+ """Return how many seconds are left until the timeout expires"""
+ if self.is_non_blocking:
+ return 0
+ elif self.is_infinite:
+ return None
+ else:
+ delta = self.target_time - self.TIME()
+ if delta > self.duration:
+ # clock jumped, recalculate
+ self.target_time = self.TIME() + self.duration
+ return self.duration
+ else:
+ return max(0, delta)
+
+ def restart(self, duration):
+ """\
+ Restart a timeout, only supported if a timeout was already set up
+ before.
+ """
+ self.duration = duration
+ self.target_time = self.TIME() + duration
+
+
+class SerialBase(io.RawIOBase):
+ """\
+ Serial port base class. Provides __init__ function and properties to
+ get/set port settings.
+ """
+
+ # default values, may be overridden in subclasses that do not support all values
+ BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
+ 9600, 19200, 38400, 57600, 115200, 230400, 460800, 500000,
+ 576000, 921600, 1000000, 1152000, 1500000, 2000000, 2500000,
+ 3000000, 3500000, 4000000)
+ BYTESIZES = (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS)
+ PARITIES = (PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE)
+ STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
+
+ def __init__(self,
+ port=None,
+ baudrate=9600,
+ bytesize=EIGHTBITS,
+ parity=PARITY_NONE,
+ stopbits=STOPBITS_ONE,
+ timeout=None,
+ xonxoff=False,
+ rtscts=False,
+ write_timeout=None,
+ dsrdtr=False,
+ inter_byte_timeout=None,
+ exclusive=None,
+ **kwargs):
+ """\
+ Initialize comm port object. If a "port" is given, then the port will be
+ opened immediately. Otherwise a Serial port object in closed state
+ is returned.
+ """
+
+ self.is_open = False
+ self.portstr = None
+ self.name = None
+ # correct values are assigned below through properties
+ self._port = None
+ self._baudrate = None
+ self._bytesize = None
+ self._parity = None
+ self._stopbits = None
+ self._timeout = None
+ self._write_timeout = None
+ self._xonxoff = None
+ self._rtscts = None
+ self._dsrdtr = None
+ self._inter_byte_timeout = None
+ self._rs485_mode = None # disabled by default
+ self._rts_state = True
+ self._dtr_state = True
+ self._break_state = False
+ self._exclusive = None
+
+ # assign values using get/set methods using the properties feature
+ self.port = port
+ self.baudrate = baudrate
+ self.bytesize = bytesize
+ self.parity = parity
+ self.stopbits = stopbits
+ self.timeout = timeout
+ self.write_timeout = write_timeout
+ self.xonxoff = xonxoff
+ self.rtscts = rtscts
+ self.dsrdtr = dsrdtr
+ self.inter_byte_timeout = inter_byte_timeout
+ self.exclusive = exclusive
+
+ # watch for backward compatible kwargs
+ if 'writeTimeout' in kwargs:
+ self.write_timeout = kwargs.pop('writeTimeout')
+ if 'interCharTimeout' in kwargs:
+ self.inter_byte_timeout = kwargs.pop('interCharTimeout')
+ if kwargs:
+ raise ValueError('unexpected keyword arguments: {!r}'.format(kwargs))
+
+ if port is not None:
+ self.open()
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+
+ # to be implemented by subclasses:
+ # def open(self):
+ # def close(self):
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+
+ @property
+ def port(self):
+ """\
+ Get the current port setting. The value that was passed on init or using
+ setPort() is passed back.
+ """
+ return self._port
+
+ @port.setter
+ def port(self, port):
+ """\
+ Change the port.
+ """
+ if port is not None and not isinstance(port, basestring):
+ raise ValueError('"port" must be None or a string, not {}'.format(type(port)))
+ was_open = self.is_open
+ if was_open:
+ self.close()
+ self.portstr = port
+ self._port = port
+ self.name = self.portstr
+ if was_open:
+ self.open()
+
+ @property
+ def baudrate(self):
+ """Get the current baud rate setting."""
+ return self._baudrate
+
+ @baudrate.setter
+ def baudrate(self, baudrate):
+ """\
+ Change baud rate. It raises a ValueError if the port is open and the
+ baud rate is not possible. If the port is closed, then the value is
+ accepted and the exception is raised when the port is opened.
+ """
+ try:
+ b = int(baudrate)
+ except TypeError:
+ raise ValueError("Not a valid baudrate: {!r}".format(baudrate))
+ else:
+ if b < 0:
+ raise ValueError("Not a valid baudrate: {!r}".format(baudrate))
+ self._baudrate = b
+ if self.is_open:
+ self._reconfigure_port()
+
+ @property
+ def bytesize(self):
+ """Get the current byte size setting."""
+ return self._bytesize
+
+ @bytesize.setter
+ def bytesize(self, bytesize):
+ """Change byte size."""
+ if bytesize not in self.BYTESIZES:
+ raise ValueError("Not a valid byte size: {!r}".format(bytesize))
+ self._bytesize = bytesize
+ if self.is_open:
+ self._reconfigure_port()
+
+ @property
+ def exclusive(self):
+ """Get the current exclusive access setting."""
+ return self._exclusive
+
+ @exclusive.setter
+ def exclusive(self, exclusive):
+ """Change the exclusive access setting."""
+ self._exclusive = exclusive
+ if self.is_open:
+ self._reconfigure_port()
+
+ @property
+ def parity(self):
+ """Get the current parity setting."""
+ return self._parity
+
+ @parity.setter
+ def parity(self, parity):
+ """Change parity setting."""
+ if parity not in self.PARITIES:
+ raise ValueError("Not a valid parity: {!r}".format(parity))
+ self._parity = parity
+ if self.is_open:
+ self._reconfigure_port()
+
+ @property
+ def stopbits(self):
+ """Get the current stop bits setting."""
+ return self._stopbits
+
+ @stopbits.setter
+ def stopbits(self, stopbits):
+ """Change stop bits size."""
+ if stopbits not in self.STOPBITS:
+ raise ValueError("Not a valid stop bit size: {!r}".format(stopbits))
+ self._stopbits = stopbits
+ if self.is_open:
+ self._reconfigure_port()
+
+ @property
+ def timeout(self):
+ """Get the current timeout setting."""
+ return self._timeout
+
+ @timeout.setter
+ def timeout(self, timeout):
+ """Change timeout setting."""
+ if timeout is not None:
+ try:
+ timeout + 1 # test if it's a number, will throw a TypeError if not...
+ except TypeError:
+ raise ValueError("Not a valid timeout: {!r}".format(timeout))
+ if timeout < 0:
+ raise ValueError("Not a valid timeout: {!r}".format(timeout))
+ self._timeout = timeout
+ if self.is_open:
+ self._reconfigure_port()
+
+ @property
+ def write_timeout(self):
+ """Get the current timeout setting."""
+ return self._write_timeout
+
+ @write_timeout.setter
+ def write_timeout(self, timeout):
+ """Change timeout setting."""
+ if timeout is not None:
+ if timeout < 0:
+ raise ValueError("Not a valid timeout: {!r}".format(timeout))
+ try:
+ timeout + 1 # test if it's a number, will throw a TypeError if not...
+ except TypeError:
+ raise ValueError("Not a valid timeout: {!r}".format(timeout))
+
+ self._write_timeout = timeout
+ if self.is_open:
+ self._reconfigure_port()
+
+ @property
+ def inter_byte_timeout(self):
+ """Get the current inter-character timeout setting."""
+ return self._inter_byte_timeout
+
+ @inter_byte_timeout.setter
+ def inter_byte_timeout(self, ic_timeout):
+ """Change inter-byte timeout setting."""
+ if ic_timeout is not None:
+ if ic_timeout < 0:
+ raise ValueError("Not a valid timeout: {!r}".format(ic_timeout))
+ try:
+ ic_timeout + 1 # test if it's a number, will throw a TypeError if not...
+ except TypeError:
+ raise ValueError("Not a valid timeout: {!r}".format(ic_timeout))
+
+ self._inter_byte_timeout = ic_timeout
+ if self.is_open:
+ self._reconfigure_port()
+
+ @property
+ def xonxoff(self):
+ """Get the current XON/XOFF setting."""
+ return self._xonxoff
+
+ @xonxoff.setter
+ def xonxoff(self, xonxoff):
+ """Change XON/XOFF setting."""
+ self._xonxoff = xonxoff
+ if self.is_open:
+ self._reconfigure_port()
+
+ @property
+ def rtscts(self):
+ """Get the current RTS/CTS flow control setting."""
+ return self._rtscts
+
+ @rtscts.setter
+ def rtscts(self, rtscts):
+ """Change RTS/CTS flow control setting."""
+ self._rtscts = rtscts
+ if self.is_open:
+ self._reconfigure_port()
+
+ @property
+ def dsrdtr(self):
+ """Get the current DSR/DTR flow control setting."""
+ return self._dsrdtr
+
+ @dsrdtr.setter
+ def dsrdtr(self, dsrdtr=None):
+ """Change DsrDtr flow control setting."""
+ if dsrdtr is None:
+ # if not set, keep backwards compatibility and follow rtscts setting
+ self._dsrdtr = self._rtscts
+ else:
+ # if defined independently, follow its value
+ self._dsrdtr = dsrdtr
+ if self.is_open:
+ self._reconfigure_port()
+
+ @property
+ def rts(self):
+ return self._rts_state
+
+ @rts.setter
+ def rts(self, value):
+ self._rts_state = value
+ if self.is_open:
+ self._update_rts_state()
+
+ @property
+ def dtr(self):
+ return self._dtr_state
+
+ @dtr.setter
+ def dtr(self, value):
+ self._dtr_state = value
+ if self.is_open:
+ self._update_dtr_state()
+
+ @property
+ def break_condition(self):
+ return self._break_state
+
+ @break_condition.setter
+ def break_condition(self, value):
+ self._break_state = value
+ if self.is_open:
+ self._update_break_state()
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+ # functions useful for RS-485 adapters
+
+ @property
+ def rs485_mode(self):
+ """\
+ Enable RS485 mode and apply new settings, set to None to disable.
+ See serial.rs485.RS485Settings for more info about the value.
+ """
+ return self._rs485_mode
+
+ @rs485_mode.setter
+ def rs485_mode(self, rs485_settings):
+ self._rs485_mode = rs485_settings
+ if self.is_open:
+ self._reconfigure_port()
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+
+ _SAVED_SETTINGS = ('baudrate', 'bytesize', 'parity', 'stopbits', 'xonxoff',
+ 'dsrdtr', 'rtscts', 'timeout', 'write_timeout',
+ 'inter_byte_timeout')
+
+ def get_settings(self):
+ """\
+ Get current port settings as a dictionary. For use with
+ apply_settings().
+ """
+ return dict([(key, getattr(self, '_' + key)) for key in self._SAVED_SETTINGS])
+
+ def apply_settings(self, d):
+ """\
+ Apply stored settings from a dictionary returned from
+ get_settings(). It's allowed to delete keys from the dictionary. These
+ values will simply left unchanged.
+ """
+ for key in self._SAVED_SETTINGS:
+ if key in d and d[key] != getattr(self, '_' + key): # check against internal "_" value
+ setattr(self, key, d[key]) # set non "_" value to use properties write function
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+
+ def __repr__(self):
+ """String representation of the current port settings and its state."""
+ return '{name}(port={p.portstr!r}, ' \
+ 'baudrate={p.baudrate!r}, bytesize={p.bytesize!r}, parity={p.parity!r}, ' \
+ 'stopbits={p.stopbits!r}, timeout={p.timeout!r}, xonxoff={p.xonxoff!r}, ' \
+ 'rtscts={p.rtscts!r}, dsrdtr={p.dsrdtr!r})'.format(
+ name=self.__class__.__name__, id=id(self), p=self)
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+ # compatibility with io library
+ # pylint: disable=invalid-name,missing-docstring
+
+ def readable(self):
+ return True
+
+ def writable(self):
+ return True
+
+ def seekable(self):
+ return False
+
+ def readinto(self, b):
+ data = self.read(len(b))
+ n = len(data)
+ try:
+ b[:n] = data
+ except TypeError as err:
+ import array
+ if not isinstance(b, array.array):
+ raise err
+ b[:n] = array.array('b', data)
+ return n
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+ # context manager
+
+ def __enter__(self):
+ if self._port is not None and not self.is_open:
+ self.open()
+ return self
+
+ def __exit__(self, *args, **kwargs):
+ self.close()
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+
+ def send_break(self, duration=0.25):
+ """\
+ Send break condition. Timed, returns to idle state after given
+ duration.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ self.break_condition = True
+ time.sleep(duration)
+ self.break_condition = False
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+ # backwards compatibility / deprecated functions
+
+ def flushInput(self):
+ self.reset_input_buffer()
+
+ def flushOutput(self):
+ self.reset_output_buffer()
+
+ def inWaiting(self):
+ return self.in_waiting
+
+ def sendBreak(self, duration=0.25):
+ self.send_break(duration)
+
+ def setRTS(self, value=1):
+ self.rts = value
+
+ def setDTR(self, value=1):
+ self.dtr = value
+
+ def getCTS(self):
+ return self.cts
+
+ def getDSR(self):
+ return self.dsr
+
+ def getRI(self):
+ return self.ri
+
+ def getCD(self):
+ return self.cd
+
+ def setPort(self, port):
+ self.port = port
+
+ @property
+ def writeTimeout(self):
+ return self.write_timeout
+
+ @writeTimeout.setter
+ def writeTimeout(self, timeout):
+ self.write_timeout = timeout
+
+ @property
+ def interCharTimeout(self):
+ return self.inter_byte_timeout
+
+ @interCharTimeout.setter
+ def interCharTimeout(self, interCharTimeout):
+ self.inter_byte_timeout = interCharTimeout
+
+ def getSettingsDict(self):
+ return self.get_settings()
+
+ def applySettingsDict(self, d):
+ self.apply_settings(d)
+
+ def isOpen(self):
+ return self.is_open
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+ # additional functionality
+
+ def read_all(self):
+ """\
+ Read all bytes currently available in the buffer of the OS.
+ """
+ return self.read(self.in_waiting)
+
+ def read_until(self, expected=LF, size=None):
+ """\
+ Read until an expected sequence is found ('\n' by default), the size
+ is exceeded or until timeout occurs.
+ """
+ lenterm = len(expected)
+ line = bytearray()
+ timeout = Timeout(self._timeout)
+ while True:
+ c = self.read(1)
+ if c:
+ line += c
+ if line[-lenterm:] == expected:
+ break
+ if size is not None and len(line) >= size:
+ break
+ else:
+ break
+ if timeout.expired():
+ break
+ return bytes(line)
+
+ def iread_until(self, *args, **kwargs):
+ """\
+ Read lines, implemented as generator. It will raise StopIteration on
+ timeout (empty read).
+ """
+ while True:
+ line = self.read_until(*args, **kwargs)
+ if not line:
+ break
+ yield line
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - -
+if __name__ == '__main__':
+ import sys
+ s = SerialBase()
+ sys.stdout.write('port name: {}\n'.format(s.name))
+ sys.stdout.write('baud rates: {}\n'.format(s.BAUDRATES))
+ sys.stdout.write('byte sizes: {}\n'.format(s.BYTESIZES))
+ sys.stdout.write('parities: {}\n'.format(s.PARITIES))
+ sys.stdout.write('stop bits: {}\n'.format(s.STOPBITS))
+ sys.stdout.write('{}\n'.format(s))
diff --git a/mixly/tools/python/serial/serialwin32.py b/mixly/tools/python/serial/serialwin32.py
new file mode 100644
index 00000000..e7da929a
--- /dev/null
+++ b/mixly/tools/python/serial/serialwin32.py
@@ -0,0 +1,477 @@
+#! python
+#
+# backend for Windows ("win32" incl. 32/64 bit support)
+#
+# (C) 2001-2020 Chris Liechti
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# Initial patch to use ctypes by Giovanni Bajo
+
+from __future__ import absolute_import
+
+# pylint: disable=invalid-name,too-few-public-methods
+import ctypes
+import time
+from serial import win32
+
+import serial
+from serial.serialutil import SerialBase, SerialException, to_bytes, PortNotOpenError, SerialTimeoutException
+
+
+class Serial(SerialBase):
+ """Serial port implementation for Win32 based on ctypes."""
+
+ BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
+ 9600, 19200, 38400, 57600, 115200)
+
+ def __init__(self, *args, **kwargs):
+ self._port_handle = None
+ self._overlapped_read = None
+ self._overlapped_write = None
+ super(Serial, self).__init__(*args, **kwargs)
+
+ def open(self):
+ """\
+ Open port with current settings. This may throw a SerialException
+ if the port cannot be opened.
+ """
+ if self._port is None:
+ raise SerialException("Port must be configured before it can be used.")
+ if self.is_open:
+ raise SerialException("Port is already open.")
+ # the "\\.\COMx" format is required for devices other than COM1-COM8
+ # not all versions of windows seem to support this properly
+ # so that the first few ports are used with the DOS device name
+ port = self.name
+ try:
+ if port.upper().startswith('COM') and int(port[3:]) > 8:
+ port = '\\\\.\\' + port
+ except ValueError:
+ # for like COMnotanumber
+ pass
+ self._port_handle = win32.CreateFile(
+ port,
+ win32.GENERIC_READ | win32.GENERIC_WRITE,
+ 0, # exclusive access
+ None, # no security
+ win32.OPEN_EXISTING,
+ win32.FILE_ATTRIBUTE_NORMAL | win32.FILE_FLAG_OVERLAPPED,
+ 0)
+ if self._port_handle == win32.INVALID_HANDLE_VALUE:
+ self._port_handle = None # 'cause __del__ is called anyway
+ raise SerialException("could not open port {!r}: {!r}".format(self.portstr, ctypes.WinError()))
+
+ try:
+ self._overlapped_read = win32.OVERLAPPED()
+ self._overlapped_read.hEvent = win32.CreateEvent(None, 1, 0, None)
+ self._overlapped_write = win32.OVERLAPPED()
+ #~ self._overlapped_write.hEvent = win32.CreateEvent(None, 1, 0, None)
+ self._overlapped_write.hEvent = win32.CreateEvent(None, 0, 0, None)
+
+ # Setup a 4k buffer
+ win32.SetupComm(self._port_handle, 4096, 4096)
+
+ # Save original timeout values:
+ self._orgTimeouts = win32.COMMTIMEOUTS()
+ win32.GetCommTimeouts(self._port_handle, ctypes.byref(self._orgTimeouts))
+
+ self._reconfigure_port()
+
+ # Clear buffers:
+ # Remove anything that was there
+ win32.PurgeComm(
+ self._port_handle,
+ win32.PURGE_TXCLEAR | win32.PURGE_TXABORT |
+ win32.PURGE_RXCLEAR | win32.PURGE_RXABORT)
+ except:
+ try:
+ self._close()
+ except:
+ # ignore any exception when closing the port
+ # also to keep original exception that happened when setting up
+ pass
+ self._port_handle = None
+ raise
+ else:
+ self.is_open = True
+
+ def _reconfigure_port(self):
+ """Set communication parameters on opened port."""
+ if not self._port_handle:
+ raise SerialException("Can only operate on a valid port handle")
+
+ # Set Windows timeout values
+ # timeouts is a tuple with the following items:
+ # (ReadIntervalTimeout,ReadTotalTimeoutMultiplier,
+ # ReadTotalTimeoutConstant,WriteTotalTimeoutMultiplier,
+ # WriteTotalTimeoutConstant)
+ timeouts = win32.COMMTIMEOUTS()
+ if self._timeout is None:
+ pass # default of all zeros is OK
+ elif self._timeout == 0:
+ timeouts.ReadIntervalTimeout = win32.MAXDWORD
+ else:
+ timeouts.ReadTotalTimeoutConstant = max(int(self._timeout * 1000), 1)
+ if self._timeout != 0 and self._inter_byte_timeout is not None:
+ timeouts.ReadIntervalTimeout = max(int(self._inter_byte_timeout * 1000), 1)
+
+ if self._write_timeout is None:
+ pass
+ elif self._write_timeout == 0:
+ timeouts.WriteTotalTimeoutConstant = win32.MAXDWORD
+ else:
+ timeouts.WriteTotalTimeoutConstant = max(int(self._write_timeout * 1000), 1)
+ win32.SetCommTimeouts(self._port_handle, ctypes.byref(timeouts))
+
+ win32.SetCommMask(self._port_handle, win32.EV_ERR)
+
+ # Setup the connection info.
+ # Get state and modify it:
+ comDCB = win32.DCB()
+ win32.GetCommState(self._port_handle, ctypes.byref(comDCB))
+ comDCB.BaudRate = self._baudrate
+
+ if self._bytesize == serial.FIVEBITS:
+ comDCB.ByteSize = 5
+ elif self._bytesize == serial.SIXBITS:
+ comDCB.ByteSize = 6
+ elif self._bytesize == serial.SEVENBITS:
+ comDCB.ByteSize = 7
+ elif self._bytesize == serial.EIGHTBITS:
+ comDCB.ByteSize = 8
+ else:
+ raise ValueError("Unsupported number of data bits: {!r}".format(self._bytesize))
+
+ if self._parity == serial.PARITY_NONE:
+ comDCB.Parity = win32.NOPARITY
+ comDCB.fParity = 0 # Disable Parity Check
+ elif self._parity == serial.PARITY_EVEN:
+ comDCB.Parity = win32.EVENPARITY
+ comDCB.fParity = 1 # Enable Parity Check
+ elif self._parity == serial.PARITY_ODD:
+ comDCB.Parity = win32.ODDPARITY
+ comDCB.fParity = 1 # Enable Parity Check
+ elif self._parity == serial.PARITY_MARK:
+ comDCB.Parity = win32.MARKPARITY
+ comDCB.fParity = 1 # Enable Parity Check
+ elif self._parity == serial.PARITY_SPACE:
+ comDCB.Parity = win32.SPACEPARITY
+ comDCB.fParity = 1 # Enable Parity Check
+ else:
+ raise ValueError("Unsupported parity mode: {!r}".format(self._parity))
+
+ if self._stopbits == serial.STOPBITS_ONE:
+ comDCB.StopBits = win32.ONESTOPBIT
+ elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
+ comDCB.StopBits = win32.ONE5STOPBITS
+ elif self._stopbits == serial.STOPBITS_TWO:
+ comDCB.StopBits = win32.TWOSTOPBITS
+ else:
+ raise ValueError("Unsupported number of stop bits: {!r}".format(self._stopbits))
+
+ comDCB.fBinary = 1 # Enable Binary Transmission
+ # Char. w/ Parity-Err are replaced with 0xff (if fErrorChar is set to TRUE)
+ if self._rs485_mode is None:
+ if self._rtscts:
+ comDCB.fRtsControl = win32.RTS_CONTROL_HANDSHAKE
+ else:
+ comDCB.fRtsControl = win32.RTS_CONTROL_ENABLE if self._rts_state else win32.RTS_CONTROL_DISABLE
+ comDCB.fOutxCtsFlow = self._rtscts
+ else:
+ # checks for unsupported settings
+ # XXX verify if platform really does not have a setting for those
+ if not self._rs485_mode.rts_level_for_tx:
+ raise ValueError(
+ 'Unsupported value for RS485Settings.rts_level_for_tx: {!r} (only True is allowed)'.format(
+ self._rs485_mode.rts_level_for_tx,))
+ if self._rs485_mode.rts_level_for_rx:
+ raise ValueError(
+ 'Unsupported value for RS485Settings.rts_level_for_rx: {!r} (only False is allowed)'.format(
+ self._rs485_mode.rts_level_for_rx,))
+ if self._rs485_mode.delay_before_tx is not None:
+ raise ValueError(
+ 'Unsupported value for RS485Settings.delay_before_tx: {!r} (only None is allowed)'.format(
+ self._rs485_mode.delay_before_tx,))
+ if self._rs485_mode.delay_before_rx is not None:
+ raise ValueError(
+ 'Unsupported value for RS485Settings.delay_before_rx: {!r} (only None is allowed)'.format(
+ self._rs485_mode.delay_before_rx,))
+ if self._rs485_mode.loopback:
+ raise ValueError(
+ 'Unsupported value for RS485Settings.loopback: {!r} (only False is allowed)'.format(
+ self._rs485_mode.loopback,))
+ comDCB.fRtsControl = win32.RTS_CONTROL_TOGGLE
+ comDCB.fOutxCtsFlow = 0
+
+ if self._dsrdtr:
+ comDCB.fDtrControl = win32.DTR_CONTROL_HANDSHAKE
+ else:
+ comDCB.fDtrControl = win32.DTR_CONTROL_ENABLE if self._dtr_state else win32.DTR_CONTROL_DISABLE
+ comDCB.fOutxDsrFlow = self._dsrdtr
+ comDCB.fOutX = self._xonxoff
+ comDCB.fInX = self._xonxoff
+ comDCB.fNull = 0
+ comDCB.fErrorChar = 0
+ comDCB.fAbortOnError = 0
+ comDCB.XonChar = serial.XON
+ comDCB.XoffChar = serial.XOFF
+
+ if not win32.SetCommState(self._port_handle, ctypes.byref(comDCB)):
+ raise SerialException(
+ 'Cannot configure port, something went wrong. '
+ 'Original message: {!r}'.format(ctypes.WinError()))
+
+ #~ def __del__(self):
+ #~ self.close()
+
+ def _close(self):
+ """internal close port helper"""
+ if self._port_handle is not None:
+ # Restore original timeout values:
+ win32.SetCommTimeouts(self._port_handle, self._orgTimeouts)
+ if self._overlapped_read is not None:
+ self.cancel_read()
+ win32.CloseHandle(self._overlapped_read.hEvent)
+ self._overlapped_read = None
+ if self._overlapped_write is not None:
+ self.cancel_write()
+ win32.CloseHandle(self._overlapped_write.hEvent)
+ self._overlapped_write = None
+ win32.CloseHandle(self._port_handle)
+ self._port_handle = None
+
+ def close(self):
+ """Close port"""
+ if self.is_open:
+ self._close()
+ self.is_open = False
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+
+ @property
+ def in_waiting(self):
+ """Return the number of bytes currently in the input buffer."""
+ flags = win32.DWORD()
+ comstat = win32.COMSTAT()
+ if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
+ raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
+ return comstat.cbInQue
+
+ def read(self, size=1):
+ """\
+ Read size bytes from the serial port. If a timeout is set it may
+ return less characters as requested. With no timeout it will block
+ until the requested number of bytes is read.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ if size > 0:
+ win32.ResetEvent(self._overlapped_read.hEvent)
+ flags = win32.DWORD()
+ comstat = win32.COMSTAT()
+ if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
+ raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
+ n = min(comstat.cbInQue, size) if self.timeout == 0 else size
+ if n > 0:
+ buf = ctypes.create_string_buffer(n)
+ rc = win32.DWORD()
+ read_ok = win32.ReadFile(
+ self._port_handle,
+ buf,
+ n,
+ ctypes.byref(rc),
+ ctypes.byref(self._overlapped_read))
+ if not read_ok and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
+ raise SerialException("ReadFile failed ({!r})".format(ctypes.WinError()))
+ result_ok = win32.GetOverlappedResult(
+ self._port_handle,
+ ctypes.byref(self._overlapped_read),
+ ctypes.byref(rc),
+ True)
+ if not result_ok:
+ if win32.GetLastError() != win32.ERROR_OPERATION_ABORTED:
+ raise SerialException("GetOverlappedResult failed ({!r})".format(ctypes.WinError()))
+ read = buf.raw[:rc.value]
+ else:
+ read = bytes()
+ else:
+ read = bytes()
+ return bytes(read)
+
+ def write(self, data):
+ """Output the given byte string over the serial port."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ #~ if not isinstance(data, (bytes, bytearray)):
+ #~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
+ # convert data (needed in case of memoryview instance: Py 3.1 io lib), ctypes doesn't like memoryview
+ data = to_bytes(data)
+ if data:
+ #~ win32event.ResetEvent(self._overlapped_write.hEvent)
+ n = win32.DWORD()
+ success = win32.WriteFile(self._port_handle, data, len(data), ctypes.byref(n), self._overlapped_write)
+ if self._write_timeout != 0: # if blocking (None) or w/ write timeout (>0)
+ if not success and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
+ raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
+
+ # Wait for the write to complete.
+ #~ win32.WaitForSingleObject(self._overlapped_write.hEvent, win32.INFINITE)
+ win32.GetOverlappedResult(self._port_handle, self._overlapped_write, ctypes.byref(n), True)
+ if win32.GetLastError() == win32.ERROR_OPERATION_ABORTED:
+ return n.value # canceled IO is no error
+ if n.value != len(data):
+ raise SerialTimeoutException('Write timeout')
+ return n.value
+ else:
+ errorcode = win32.ERROR_SUCCESS if success else win32.GetLastError()
+ if errorcode in (win32.ERROR_INVALID_USER_BUFFER, win32.ERROR_NOT_ENOUGH_MEMORY,
+ win32.ERROR_OPERATION_ABORTED):
+ return 0
+ elif errorcode in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
+ # no info on true length provided by OS function in async mode
+ return len(data)
+ else:
+ raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
+ else:
+ return 0
+
+ def flush(self):
+ """\
+ Flush of file like objects. In this case, wait until all data
+ is written.
+ """
+ while self.out_waiting:
+ time.sleep(0.05)
+ # XXX could also use WaitCommEvent with mask EV_TXEMPTY, but it would
+ # require overlapped IO and it's also only possible to set a single mask
+ # on the port---
+
+ def reset_input_buffer(self):
+ """Clear input buffer, discarding all that is in the buffer."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ win32.PurgeComm(self._port_handle, win32.PURGE_RXCLEAR | win32.PURGE_RXABORT)
+
+ def reset_output_buffer(self):
+ """\
+ Clear output buffer, aborting the current output and discarding all
+ that is in the buffer.
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ win32.PurgeComm(self._port_handle, win32.PURGE_TXCLEAR | win32.PURGE_TXABORT)
+
+ def _update_break_state(self):
+ """Set break: Controls TXD. When active, to transmitting is possible."""
+ if not self.is_open:
+ raise PortNotOpenError()
+ if self._break_state:
+ win32.SetCommBreak(self._port_handle)
+ else:
+ win32.ClearCommBreak(self._port_handle)
+
+ def _update_rts_state(self):
+ """Set terminal status line: Request To Send"""
+ if self._rts_state:
+ win32.EscapeCommFunction(self._port_handle, win32.SETRTS)
+ else:
+ win32.EscapeCommFunction(self._port_handle, win32.CLRRTS)
+
+ def _update_dtr_state(self):
+ """Set terminal status line: Data Terminal Ready"""
+ if self._dtr_state:
+ win32.EscapeCommFunction(self._port_handle, win32.SETDTR)
+ else:
+ win32.EscapeCommFunction(self._port_handle, win32.CLRDTR)
+
+ def _GetCommModemStatus(self):
+ if not self.is_open:
+ raise PortNotOpenError()
+ stat = win32.DWORD()
+ win32.GetCommModemStatus(self._port_handle, ctypes.byref(stat))
+ return stat.value
+
+ @property
+ def cts(self):
+ """Read terminal status line: Clear To Send"""
+ return win32.MS_CTS_ON & self._GetCommModemStatus() != 0
+
+ @property
+ def dsr(self):
+ """Read terminal status line: Data Set Ready"""
+ return win32.MS_DSR_ON & self._GetCommModemStatus() != 0
+
+ @property
+ def ri(self):
+ """Read terminal status line: Ring Indicator"""
+ return win32.MS_RING_ON & self._GetCommModemStatus() != 0
+
+ @property
+ def cd(self):
+ """Read terminal status line: Carrier Detect"""
+ return win32.MS_RLSD_ON & self._GetCommModemStatus() != 0
+
+ # - - platform specific - - - -
+
+ def set_buffer_size(self, rx_size=4096, tx_size=None):
+ """\
+ Recommend a buffer size to the driver (device driver can ignore this
+ value). Must be called after the port is opened.
+ """
+ if tx_size is None:
+ tx_size = rx_size
+ win32.SetupComm(self._port_handle, rx_size, tx_size)
+
+ def set_output_flow_control(self, enable=True):
+ """\
+ Manually control flow - when software flow control is enabled.
+ This will do the same as if XON (true) or XOFF (false) are received
+ from the other device and control the transmission accordingly.
+ WARNING: this function is not portable to different platforms!
+ """
+ if not self.is_open:
+ raise PortNotOpenError()
+ if enable:
+ win32.EscapeCommFunction(self._port_handle, win32.SETXON)
+ else:
+ win32.EscapeCommFunction(self._port_handle, win32.SETXOFF)
+
+ @property
+ def out_waiting(self):
+ """Return how many bytes the in the outgoing buffer"""
+ flags = win32.DWORD()
+ comstat = win32.COMSTAT()
+ if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
+ raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
+ return comstat.cbOutQue
+
+ def _cancel_overlapped_io(self, overlapped):
+ """Cancel a blocking read operation, may be called from other thread"""
+ # check if read operation is pending
+ rc = win32.DWORD()
+ err = win32.GetOverlappedResult(
+ self._port_handle,
+ ctypes.byref(overlapped),
+ ctypes.byref(rc),
+ False)
+ if not err and win32.GetLastError() in (win32.ERROR_IO_PENDING, win32.ERROR_IO_INCOMPLETE):
+ # cancel, ignoring any errors (e.g. it may just have finished on its own)
+ win32.CancelIoEx(self._port_handle, overlapped)
+
+ def cancel_read(self):
+ """Cancel a blocking read operation, may be called from other thread"""
+ self._cancel_overlapped_io(self._overlapped_read)
+
+ def cancel_write(self):
+ """Cancel a blocking write operation, may be called from other thread"""
+ self._cancel_overlapped_io(self._overlapped_write)
+
+ @SerialBase.exclusive.setter
+ def exclusive(self, exclusive):
+ """Change the exclusive access setting."""
+ if exclusive is not None and not exclusive:
+ raise ValueError('win32 only supports exclusive access (not: {})'.format(exclusive))
+ else:
+ serial.SerialBase.exclusive.__set__(self, exclusive)
diff --git a/mixly/tools/python/serial/threaded/__init__.py b/mixly/tools/python/serial/threaded/__init__.py
new file mode 100644
index 00000000..b8940b6d
--- /dev/null
+++ b/mixly/tools/python/serial/threaded/__init__.py
@@ -0,0 +1,297 @@
+#!/usr/bin/env python3
+#
+# Working with threading and pySerial
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2015-2016 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+"""\
+Support threading with serial ports.
+"""
+from __future__ import absolute_import
+
+import serial
+import threading
+
+
+class Protocol(object):
+ """\
+ Protocol as used by the ReaderThread. This base class provides empty
+ implementations of all methods.
+ """
+
+ def connection_made(self, transport):
+ """Called when reader thread is started"""
+
+ def data_received(self, data):
+ """Called with snippets received from the serial port"""
+
+ def connection_lost(self, exc):
+ """\
+ Called when the serial port is closed or the reader loop terminated
+ otherwise.
+ """
+ if isinstance(exc, Exception):
+ raise exc
+
+
+class Packetizer(Protocol):
+ """
+ Read binary packets from serial port. Packets are expected to be terminated
+ with a TERMINATOR byte (null byte by default).
+
+ The class also keeps track of the transport.
+ """
+
+ TERMINATOR = b'\0'
+
+ def __init__(self):
+ self.buffer = bytearray()
+ self.transport = None
+
+ def connection_made(self, transport):
+ """Store transport"""
+ self.transport = transport
+
+ def connection_lost(self, exc):
+ """Forget transport"""
+ self.transport = None
+ super(Packetizer, self).connection_lost(exc)
+
+ def data_received(self, data):
+ """Buffer received data, find TERMINATOR, call handle_packet"""
+ self.buffer.extend(data)
+ while self.TERMINATOR in self.buffer:
+ packet, self.buffer = self.buffer.split(self.TERMINATOR, 1)
+ self.handle_packet(packet)
+
+ def handle_packet(self, packet):
+ """Process packets - to be overridden by subclassing"""
+ raise NotImplementedError('please implement functionality in handle_packet')
+
+
+class FramedPacket(Protocol):
+ """
+ Read binary packets. Packets are expected to have a start and stop marker.
+
+ The class also keeps track of the transport.
+ """
+
+ START = b'('
+ STOP = b')'
+
+ def __init__(self):
+ self.packet = bytearray()
+ self.in_packet = False
+ self.transport = None
+
+ def connection_made(self, transport):
+ """Store transport"""
+ self.transport = transport
+
+ def connection_lost(self, exc):
+ """Forget transport"""
+ self.transport = None
+ self.in_packet = False
+ del self.packet[:]
+ super(FramedPacket, self).connection_lost(exc)
+
+ def data_received(self, data):
+ """Find data enclosed in START/STOP, call handle_packet"""
+ for byte in serial.iterbytes(data):
+ if byte == self.START:
+ self.in_packet = True
+ elif byte == self.STOP:
+ self.in_packet = False
+ self.handle_packet(bytes(self.packet)) # make read-only copy
+ del self.packet[:]
+ elif self.in_packet:
+ self.packet.extend(byte)
+ else:
+ self.handle_out_of_packet_data(byte)
+
+ def handle_packet(self, packet):
+ """Process packets - to be overridden by subclassing"""
+ raise NotImplementedError('please implement functionality in handle_packet')
+
+ def handle_out_of_packet_data(self, data):
+ """Process data that is received outside of packets"""
+ pass
+
+
+class LineReader(Packetizer):
+ """
+ Read and write (Unicode) lines from/to serial port.
+ The encoding is applied.
+ """
+
+ TERMINATOR = b'\r\n'
+ ENCODING = 'utf-8'
+ UNICODE_HANDLING = 'replace'
+
+ def handle_packet(self, packet):
+ self.handle_line(packet.decode(self.ENCODING, self.UNICODE_HANDLING))
+
+ def handle_line(self, line):
+ """Process one line - to be overridden by subclassing"""
+ raise NotImplementedError('please implement functionality in handle_line')
+
+ def write_line(self, text):
+ """
+ Write text to the transport. ``text`` is a Unicode string and the encoding
+ is applied before sending ans also the newline is append.
+ """
+ # + is not the best choice but bytes does not support % or .format in py3 and we want a single write call
+ self.transport.write(text.encode(self.ENCODING, self.UNICODE_HANDLING) + self.TERMINATOR)
+
+
+class ReaderThread(threading.Thread):
+ """\
+ Implement a serial port read loop and dispatch to a Protocol instance (like
+ the asyncio.Protocol) but do it with threads.
+
+ Calls to close() will close the serial port but it is also possible to just
+ stop() this thread and continue the serial port instance otherwise.
+ """
+
+ def __init__(self, serial_instance, protocol_factory):
+ """\
+ Initialize thread.
+
+ Note that the serial_instance' timeout is set to one second!
+ Other settings are not changed.
+ """
+ super(ReaderThread, self).__init__()
+ self.daemon = True
+ self.serial = serial_instance
+ self.protocol_factory = protocol_factory
+ self.alive = True
+ self._lock = threading.Lock()
+ self._connection_made = threading.Event()
+ self.protocol = None
+
+ def stop(self):
+ """Stop the reader thread"""
+ self.alive = False
+ if hasattr(self.serial, 'cancel_read'):
+ self.serial.cancel_read()
+ self.join(2)
+
+ def run(self):
+ """Reader loop"""
+ if not hasattr(self.serial, 'cancel_read'):
+ self.serial.timeout = 1
+ self.protocol = self.protocol_factory()
+ try:
+ self.protocol.connection_made(self)
+ except Exception as e:
+ self.alive = False
+ self.protocol.connection_lost(e)
+ self._connection_made.set()
+ return
+ error = None
+ self._connection_made.set()
+ while self.alive and self.serial.is_open:
+ try:
+ # read all that is there or wait for one byte (blocking)
+ data = self.serial.read(self.serial.in_waiting or 1)
+ except serial.SerialException as e:
+ # probably some I/O problem such as disconnected USB serial
+ # adapters -> exit
+ error = e
+ break
+ else:
+ if data:
+ # make a separated try-except for called user code
+ try:
+ self.protocol.data_received(data)
+ except Exception as e:
+ error = e
+ break
+ self.alive = False
+ self.protocol.connection_lost(error)
+ self.protocol = None
+
+ def write(self, data):
+ """Thread safe writing (uses lock)"""
+ with self._lock:
+ return self.serial.write(data)
+
+ def close(self):
+ """Close the serial port and exit reader thread (uses lock)"""
+ # use the lock to let other threads finish writing
+ with self._lock:
+ # first stop reading, so that closing can be done on idle port
+ self.stop()
+ self.serial.close()
+
+ def connect(self):
+ """
+ Wait until connection is set up and return the transport and protocol
+ instances.
+ """
+ if self.alive:
+ self._connection_made.wait()
+ if not self.alive:
+ raise RuntimeError('connection_lost already called')
+ return (self, self.protocol)
+ else:
+ raise RuntimeError('already stopped')
+
+ # - - context manager, returns protocol
+
+ def __enter__(self):
+ """\
+ Enter context handler. May raise RuntimeError in case the connection
+ could not be created.
+ """
+ self.start()
+ self._connection_made.wait()
+ if not self.alive:
+ raise RuntimeError('connection_lost already called')
+ return self.protocol
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """Leave context: close port"""
+ self.close()
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# test
+if __name__ == '__main__':
+ # pylint: disable=wrong-import-position
+ import sys
+ import time
+ import traceback
+
+ #~ PORT = 'spy:///dev/ttyUSB0'
+ PORT = 'loop://'
+
+ class PrintLines(LineReader):
+ def connection_made(self, transport):
+ super(PrintLines, self).connection_made(transport)
+ sys.stdout.write('port opened\n')
+ self.write_line('hello world')
+
+ def handle_line(self, data):
+ sys.stdout.write('line received: {!r}\n'.format(data))
+
+ def connection_lost(self, exc):
+ if exc:
+ traceback.print_exc(exc)
+ sys.stdout.write('port closed\n')
+
+ ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1)
+ with ReaderThread(ser, PrintLines) as protocol:
+ protocol.write_line('hello')
+ time.sleep(2)
+
+ # alternative usage
+ ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1)
+ t = ReaderThread(ser, PrintLines)
+ t.start()
+ transport, protocol = t.connect()
+ protocol.write_line('hello')
+ time.sleep(2)
+ t.close()
diff --git a/mixly/tools/python/serial/tools/__init__.py b/mixly/tools/python/serial/tools/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mixly/tools/python/serial/tools/hexlify_codec.py b/mixly/tools/python/serial/tools/hexlify_codec.py
new file mode 100644
index 00000000..bd8f6b0d
--- /dev/null
+++ b/mixly/tools/python/serial/tools/hexlify_codec.py
@@ -0,0 +1,126 @@
+#! python
+#
+# This is a codec to create and decode hexdumps with spaces between characters. used by miniterm.
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2015-2016 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+"""\
+Python 'hex' Codec - 2-digit hex with spaces content transfer encoding.
+
+Encode and decode may be a bit missleading at first sight...
+
+The textual representation is a hex dump: e.g. "40 41"
+The "encoded" data of this is the binary form, e.g. b"@A"
+
+Therefore decoding is binary to text and thus converting binary data to hex dump.
+
+"""
+
+from __future__ import absolute_import
+
+import codecs
+import serial
+
+
+try:
+ unicode
+except (NameError, AttributeError):
+ unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name
+
+
+HEXDIGITS = '0123456789ABCDEF'
+
+
+# Codec APIs
+
+def hex_encode(data, errors='strict'):
+ """'40 41 42' -> b'@ab'"""
+ return (serial.to_bytes([int(h, 16) for h in data.split()]), len(data))
+
+
+def hex_decode(data, errors='strict'):
+ """b'@ab' -> '40 41 42'"""
+ return (unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data))), len(data))
+
+
+class Codec(codecs.Codec):
+ def encode(self, data, errors='strict'):
+ """'40 41 42' -> b'@ab'"""
+ return serial.to_bytes([int(h, 16) for h in data.split()])
+
+ def decode(self, data, errors='strict'):
+ """b'@ab' -> '40 41 42'"""
+ return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data)))
+
+
+class IncrementalEncoder(codecs.IncrementalEncoder):
+ """Incremental hex encoder"""
+
+ def __init__(self, errors='strict'):
+ self.errors = errors
+ self.state = 0
+
+ def reset(self):
+ self.state = 0
+
+ def getstate(self):
+ return self.state
+
+ def setstate(self, state):
+ self.state = state
+
+ def encode(self, data, final=False):
+ """\
+ Incremental encode, keep track of digits and emit a byte when a pair
+ of hex digits is found. The space is optional unless the error
+ handling is defined to be 'strict'.
+ """
+ state = self.state
+ encoded = []
+ for c in data.upper():
+ if c in HEXDIGITS:
+ z = HEXDIGITS.index(c)
+ if state:
+ encoded.append(z + (state & 0xf0))
+ state = 0
+ else:
+ state = 0x100 + (z << 4)
+ elif c == ' ': # allow spaces to separate values
+ if state and self.errors == 'strict':
+ raise UnicodeError('odd number of hex digits')
+ state = 0
+ else:
+ if self.errors == 'strict':
+ raise UnicodeError('non-hex digit found: {!r}'.format(c))
+ self.state = state
+ return serial.to_bytes(encoded)
+
+
+class IncrementalDecoder(codecs.IncrementalDecoder):
+ """Incremental decoder"""
+ def decode(self, data, final=False):
+ return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data)))
+
+
+class StreamWriter(Codec, codecs.StreamWriter):
+ """Combination of hexlify codec and StreamWriter"""
+
+
+class StreamReader(Codec, codecs.StreamReader):
+ """Combination of hexlify codec and StreamReader"""
+
+
+def getregentry():
+ """encodings module API"""
+ return codecs.CodecInfo(
+ name='hexlify',
+ encode=hex_encode,
+ decode=hex_decode,
+ incrementalencoder=IncrementalEncoder,
+ incrementaldecoder=IncrementalDecoder,
+ streamwriter=StreamWriter,
+ streamreader=StreamReader,
+ #~ _is_text_encoding=True,
+ )
diff --git a/mixly/tools/python/serial/tools/list_ports.py b/mixly/tools/python/serial/tools/list_ports.py
new file mode 100644
index 00000000..0d7e3d41
--- /dev/null
+++ b/mixly/tools/python/serial/tools/list_ports.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+#
+# Serial port enumeration. Console tool and backend selection.
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2011-2015 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""\
+This module will provide a function called comports that returns an
+iterable (generator or list) that will enumerate available com ports. Note that
+on some systems non-existent ports may be listed.
+
+Additionally a grep function is supplied that can be used to search for ports
+based on their descriptions or hardware ID.
+"""
+
+from __future__ import absolute_import
+
+import sys
+import os
+import re
+
+# chose an implementation, depending on os
+#~ if sys.platform == 'cli':
+#~ else:
+if os.name == 'nt': # sys.platform == 'win32':
+ from serial.tools.list_ports_windows import comports
+elif os.name == 'posix':
+ from serial.tools.list_ports_posix import comports
+#~ elif os.name == 'java':
+else:
+ raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def grep(regexp, include_links=False):
+ """\
+ Search for ports using a regular expression. Port name, description and
+ hardware ID are searched. The function returns an iterable that returns the
+ same tuples as comport() would do.
+ """
+ r = re.compile(regexp, re.I)
+ for info in comports(include_links):
+ port, desc, hwid = info
+ if r.search(port) or r.search(desc) or r.search(hwid):
+ yield info
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+def main():
+ import argparse
+
+ parser = argparse.ArgumentParser(description='Serial port enumeration')
+
+ parser.add_argument(
+ 'regexp',
+ nargs='?',
+ help='only show ports that match this regex')
+
+ parser.add_argument(
+ '-v', '--verbose',
+ action='store_true',
+ help='show more messages')
+
+ parser.add_argument(
+ '-q', '--quiet',
+ action='store_true',
+ help='suppress all messages')
+
+ parser.add_argument(
+ '-n',
+ type=int,
+ help='only output the N-th entry')
+
+ parser.add_argument(
+ '-s', '--include-links',
+ action='store_true',
+ help='include entries that are symlinks to real devices')
+
+ args = parser.parse_args()
+
+ hits = 0
+ # get iteraror w/ or w/o filter
+ if args.regexp:
+ if not args.quiet:
+ sys.stderr.write("Filtered list with regexp: {!r}\n".format(args.regexp))
+ iterator = sorted(grep(args.regexp, include_links=args.include_links))
+ else:
+ iterator = sorted(comports(include_links=args.include_links))
+ # list them
+ for n, (port, desc, hwid) in enumerate(iterator, 1):
+ if args.n is None or args.n == n:
+ sys.stdout.write("{:20}\n".format(port))
+ if args.verbose:
+ sys.stdout.write(" desc: {}\n".format(desc))
+ sys.stdout.write(" hwid: {}\n".format(hwid))
+ hits += 1
+ if not args.quiet:
+ if hits:
+ sys.stderr.write("{} ports found\n".format(hits))
+ else:
+ sys.stderr.write("no ports found\n")
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# test
+if __name__ == '__main__':
+ main()
diff --git a/mixly/tools/python/serial/tools/list_ports_common.py b/mixly/tools/python/serial/tools/list_ports_common.py
new file mode 100644
index 00000000..617f3dc1
--- /dev/null
+++ b/mixly/tools/python/serial/tools/list_ports_common.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python
+#
+# This is a helper module for the various platform dependent list_port
+# implementations.
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2015 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import absolute_import
+
+import re
+import glob
+import os
+import os.path
+
+
+def numsplit(text):
+ """\
+ Convert string into a list of texts and numbers in order to support a
+ natural sorting.
+ """
+ result = []
+ for group in re.split(r'(\d+)', text):
+ if group:
+ try:
+ group = int(group)
+ except ValueError:
+ pass
+ result.append(group)
+ return result
+
+
+class ListPortInfo(object):
+ """Info collection base class for serial ports"""
+
+ def __init__(self, device, skip_link_detection=False):
+ self.device = device
+ self.name = os.path.basename(device)
+ self.description = 'n/a'
+ self.hwid = 'n/a'
+ # USB specific data
+ self.vid = None
+ self.pid = None
+ self.serial_number = None
+ self.location = None
+ self.manufacturer = None
+ self.product = None
+ self.interface = None
+ # special handling for links
+ if not skip_link_detection and device is not None and os.path.islink(device):
+ self.hwid = 'LINK={}'.format(os.path.realpath(device))
+
+ def usb_description(self):
+ """return a short string to name the port based on USB info"""
+ if self.interface is not None:
+ return '{} - {}'.format(self.product, self.interface)
+ elif self.product is not None:
+ return self.product
+ else:
+ return self.name
+
+ def usb_info(self):
+ """return a string with USB related information about device"""
+ return 'USB VID:PID={:04X}:{:04X}{}{}'.format(
+ self.vid or 0,
+ self.pid or 0,
+ ' SER={}'.format(self.serial_number) if self.serial_number is not None else '',
+ ' LOCATION={}'.format(self.location) if self.location is not None else '')
+
+ def apply_usb_info(self):
+ """update description and hwid from USB data"""
+ self.description = self.usb_description()
+ self.hwid = self.usb_info()
+
+ def __eq__(self, other):
+ return isinstance(other, ListPortInfo) and self.device == other.device
+
+ def __hash__(self):
+ return hash(self.device)
+
+ def __lt__(self, other):
+ if not isinstance(other, ListPortInfo):
+ raise TypeError('unorderable types: {}() and {}()'.format(
+ type(self).__name__,
+ type(other).__name__))
+ return numsplit(self.device) < numsplit(other.device)
+
+ def __str__(self):
+ return '{} - {}'.format(self.device, self.description)
+
+ def __getitem__(self, index):
+ """Item access: backwards compatible -> (port, desc, hwid)"""
+ if index == 0:
+ return self.device
+ elif index == 1:
+ return self.description
+ elif index == 2:
+ return self.hwid
+ else:
+ raise IndexError('{} > 2'.format(index))
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+def list_links(devices):
+ """\
+ search all /dev devices and look for symlinks to known ports already
+ listed in devices.
+ """
+ links = []
+ for device in glob.glob('/dev/*'):
+ if os.path.islink(device) and os.path.realpath(device) in devices:
+ links.append(device)
+ return links
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# test
+if __name__ == '__main__':
+ print(ListPortInfo('dummy'))
diff --git a/mixly/tools/python/serial/tools/list_ports_linux.py b/mixly/tools/python/serial/tools/list_ports_linux.py
new file mode 100644
index 00000000..c8c1cfc0
--- /dev/null
+++ b/mixly/tools/python/serial/tools/list_ports_linux.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+#
+# This is a module that gathers a list of serial ports including details on
+# GNU/Linux systems.
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2011-2015 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import absolute_import
+
+import glob
+import os
+from serial.tools import list_ports_common
+
+
+class SysFS(list_ports_common.ListPortInfo):
+ """Wrapper for easy sysfs access and device info"""
+
+ def __init__(self, device):
+ super(SysFS, self).__init__(device)
+ # special handling for links
+ if device is not None and os.path.islink(device):
+ device = os.path.realpath(device)
+ is_link = True
+ else:
+ is_link = False
+ self.usb_device_path = None
+ if os.path.exists('/sys/class/tty/{}/device'.format(self.name)):
+ self.device_path = os.path.realpath('/sys/class/tty/{}/device'.format(self.name))
+ self.subsystem = os.path.basename(os.path.realpath(os.path.join(self.device_path, 'subsystem')))
+ else:
+ self.device_path = None
+ self.subsystem = None
+ # check device type
+ if self.subsystem == 'usb-serial':
+ self.usb_interface_path = os.path.dirname(self.device_path)
+ elif self.subsystem == 'usb':
+ self.usb_interface_path = self.device_path
+ else:
+ self.usb_interface_path = None
+ # fill-in info for USB devices
+ if self.usb_interface_path is not None:
+ self.usb_device_path = os.path.dirname(self.usb_interface_path)
+
+ try:
+ num_if = int(self.read_line(self.usb_device_path, 'bNumInterfaces'))
+ except ValueError:
+ num_if = 1
+
+ self.vid = int(self.read_line(self.usb_device_path, 'idVendor'), 16)
+ self.pid = int(self.read_line(self.usb_device_path, 'idProduct'), 16)
+ self.serial_number = self.read_line(self.usb_device_path, 'serial')
+ if num_if > 1: # multi interface devices like FT4232
+ self.location = os.path.basename(self.usb_interface_path)
+ else:
+ self.location = os.path.basename(self.usb_device_path)
+
+ self.manufacturer = self.read_line(self.usb_device_path, 'manufacturer')
+ self.product = self.read_line(self.usb_device_path, 'product')
+ self.interface = self.read_line(self.usb_interface_path, 'interface')
+
+ if self.subsystem in ('usb', 'usb-serial'):
+ self.apply_usb_info()
+ #~ elif self.subsystem in ('pnp', 'amba'): # PCI based devices, raspi
+ elif self.subsystem == 'pnp': # PCI based devices
+ self.description = self.name
+ self.hwid = self.read_line(self.device_path, 'id')
+ elif self.subsystem == 'amba': # raspi
+ self.description = self.name
+ self.hwid = os.path.basename(self.device_path)
+
+ if is_link:
+ self.hwid += ' LINK={}'.format(device)
+
+ def read_line(self, *args):
+ """\
+ Helper function to read a single line from a file.
+ One or more parameters are allowed, they are joined with os.path.join.
+ Returns None on errors..
+ """
+ try:
+ with open(os.path.join(*args)) as f:
+ line = f.readline().strip()
+ return line
+ except IOError:
+ return None
+
+
+def comports(include_links=False):
+ devices = glob.glob('/dev/ttyS*') # built-in serial ports
+ devices.extend(glob.glob('/dev/ttyUSB*')) # usb-serial with own driver
+ devices.extend(glob.glob('/dev/ttyXRUSB*')) # xr-usb-serial port exar (DELL Edge 3001)
+ devices.extend(glob.glob('/dev/ttyACM*')) # usb-serial with CDC-ACM profile
+ devices.extend(glob.glob('/dev/ttyAMA*')) # ARM internal port (raspi)
+ devices.extend(glob.glob('/dev/rfcomm*')) # BT serial devices
+ devices.extend(glob.glob('/dev/ttyAP*')) # Advantech multi-port serial controllers
+ if include_links:
+ devices.extend(list_ports_common.list_links(devices))
+ return [info
+ for info in [SysFS(d) for d in devices]
+ if info.subsystem != "platform"] # hide non-present internal serial ports
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# test
+if __name__ == '__main__':
+ for info in sorted(comports()):
+ print("{0}: {0.subsystem}".format(info))
diff --git a/mixly/tools/python/serial/tools/list_ports_osx.py b/mixly/tools/python/serial/tools/list_ports_osx.py
new file mode 100644
index 00000000..51b4e8c0
--- /dev/null
+++ b/mixly/tools/python/serial/tools/list_ports_osx.py
@@ -0,0 +1,299 @@
+#!/usr/bin/env python
+#
+# This is a module that gathers a list of serial ports including details on OSX
+#
+# code originally from https://github.com/makerbot/pyserial/tree/master/serial/tools
+# with contributions from cibomahto, dgs3, FarMcKon, tedbrandston
+# and modifications by cliechti, hoihu, hardkrash
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2013-2020
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+
+# List all of the callout devices in OS/X by querying IOKit.
+
+# See the following for a reference of how to do this:
+# http://developer.apple.com/library/mac/#documentation/DeviceDrivers/Conceptual/WorkingWSerial/WWSerial_SerialDevs/SerialDevices.html#//apple_ref/doc/uid/TP30000384-CIHGEAFD
+
+# More help from darwin_hid.py
+
+# Also see the 'IORegistryExplorer' for an idea of what we are actually searching
+
+from __future__ import absolute_import
+
+import ctypes
+
+from serial.tools import list_ports_common
+
+iokit = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/IOKit.framework/IOKit')
+cf = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation')
+
+# kIOMasterPortDefault is no longer exported in BigSur but no biggie, using NULL works just the same
+kIOMasterPortDefault = 0 # WAS: ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault")
+kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault")
+
+kCFStringEncodingMacRoman = 0
+kCFStringEncodingUTF8 = 0x08000100
+
+# defined in `IOKit/usb/USBSpec.h`
+kUSBVendorString = 'USB Vendor Name'
+kUSBSerialNumberString = 'USB Serial Number'
+
+# `io_name_t` defined as `typedef char io_name_t[128];`
+# in `device/device_types.h`
+io_name_size = 128
+
+# defined in `mach/kern_return.h`
+KERN_SUCCESS = 0
+# kern_return_t defined as `typedef int kern_return_t;` in `mach/i386/kern_return.h`
+kern_return_t = ctypes.c_int
+
+iokit.IOServiceMatching.restype = ctypes.c_void_p
+
+iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
+iokit.IOServiceGetMatchingServices.restype = kern_return_t
+
+iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
+iokit.IOServiceGetMatchingServices.restype = kern_return_t
+
+iokit.IORegistryEntryCreateCFProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32]
+iokit.IORegistryEntryCreateCFProperty.restype = ctypes.c_void_p
+
+iokit.IORegistryEntryGetPath.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
+iokit.IORegistryEntryGetPath.restype = kern_return_t
+
+iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
+iokit.IORegistryEntryGetName.restype = kern_return_t
+
+iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
+iokit.IOObjectGetClass.restype = kern_return_t
+
+iokit.IOObjectRelease.argtypes = [ctypes.c_void_p]
+
+
+cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32]
+cf.CFStringCreateWithCString.restype = ctypes.c_void_p
+
+cf.CFStringGetCStringPtr.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
+cf.CFStringGetCStringPtr.restype = ctypes.c_char_p
+
+cf.CFStringGetCString.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long, ctypes.c_uint32]
+cf.CFStringGetCString.restype = ctypes.c_bool
+
+cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p]
+cf.CFNumberGetValue.restype = ctypes.c_void_p
+
+# void CFRelease ( CFTypeRef cf );
+cf.CFRelease.argtypes = [ctypes.c_void_p]
+cf.CFRelease.restype = None
+
+# CFNumber type defines
+kCFNumberSInt8Type = 1
+kCFNumberSInt16Type = 2
+kCFNumberSInt32Type = 3
+kCFNumberSInt64Type = 4
+
+
+def get_string_property(device_type, property):
+ """
+ Search the given device for the specified string property
+
+ @param device_type Type of Device
+ @param property String to search for
+ @return Python string containing the value, or None if not found.
+ """
+ key = cf.CFStringCreateWithCString(
+ kCFAllocatorDefault,
+ property.encode("utf-8"),
+ kCFStringEncodingUTF8)
+
+ CFContainer = iokit.IORegistryEntryCreateCFProperty(
+ device_type,
+ key,
+ kCFAllocatorDefault,
+ 0)
+ output = None
+
+ if CFContainer:
+ output = cf.CFStringGetCStringPtr(CFContainer, 0)
+ if output is not None:
+ output = output.decode('utf-8')
+ else:
+ buffer = ctypes.create_string_buffer(io_name_size);
+ success = cf.CFStringGetCString(CFContainer, ctypes.byref(buffer), io_name_size, kCFStringEncodingUTF8)
+ if success:
+ output = buffer.value.decode('utf-8')
+ cf.CFRelease(CFContainer)
+ return output
+
+
+def get_int_property(device_type, property, cf_number_type):
+ """
+ Search the given device for the specified string property
+
+ @param device_type Device to search
+ @param property String to search for
+ @param cf_number_type CFType number
+
+ @return Python string containing the value, or None if not found.
+ """
+ key = cf.CFStringCreateWithCString(
+ kCFAllocatorDefault,
+ property.encode("utf-8"),
+ kCFStringEncodingUTF8)
+
+ CFContainer = iokit.IORegistryEntryCreateCFProperty(
+ device_type,
+ key,
+ kCFAllocatorDefault,
+ 0)
+
+ if CFContainer:
+ if (cf_number_type == kCFNumberSInt32Type):
+ number = ctypes.c_uint32()
+ elif (cf_number_type == kCFNumberSInt16Type):
+ number = ctypes.c_uint16()
+ cf.CFNumberGetValue(CFContainer, cf_number_type, ctypes.byref(number))
+ cf.CFRelease(CFContainer)
+ return number.value
+ return None
+
+def IORegistryEntryGetName(device):
+ devicename = ctypes.create_string_buffer(io_name_size);
+ res = iokit.IORegistryEntryGetName(device, ctypes.byref(devicename))
+ if res != KERN_SUCCESS:
+ return None
+ # this works in python2 but may not be valid. Also I don't know if
+ # this encoding is guaranteed. It may be dependent on system locale.
+ return devicename.value.decode('utf-8')
+
+def IOObjectGetClass(device):
+ classname = ctypes.create_string_buffer(io_name_size)
+ iokit.IOObjectGetClass(device, ctypes.byref(classname))
+ return classname.value
+
+def GetParentDeviceByType(device, parent_type):
+ """ Find the first parent of a device that implements the parent_type
+ @param IOService Service to inspect
+ @return Pointer to the parent type, or None if it was not found.
+ """
+ # First, try to walk up the IOService tree to find a parent of this device that is a IOUSBDevice.
+ parent_type = parent_type.encode('utf-8')
+ while IOObjectGetClass(device) != parent_type:
+ parent = ctypes.c_void_p()
+ response = iokit.IORegistryEntryGetParentEntry(
+ device,
+ "IOService".encode("utf-8"),
+ ctypes.byref(parent))
+ # If we weren't able to find a parent for the device, we're done.
+ if response != KERN_SUCCESS:
+ return None
+ device = parent
+ return device
+
+
+def GetIOServicesByType(service_type):
+ """
+ returns iterator over specified service_type
+ """
+ serial_port_iterator = ctypes.c_void_p()
+
+ iokit.IOServiceGetMatchingServices(
+ kIOMasterPortDefault,
+ iokit.IOServiceMatching(service_type.encode('utf-8')),
+ ctypes.byref(serial_port_iterator))
+
+ services = []
+ while iokit.IOIteratorIsValid(serial_port_iterator):
+ service = iokit.IOIteratorNext(serial_port_iterator)
+ if not service:
+ break
+ services.append(service)
+ iokit.IOObjectRelease(serial_port_iterator)
+ return services
+
+
+def location_to_string(locationID):
+ """
+ helper to calculate port and bus number from locationID
+ """
+ loc = ['{}-'.format(locationID >> 24)]
+ while locationID & 0xf00000:
+ if len(loc) > 1:
+ loc.append('.')
+ loc.append('{}'.format((locationID >> 20) & 0xf))
+ locationID <<= 4
+ return ''.join(loc)
+
+
+class SuitableSerialInterface(object):
+ pass
+
+
+def scan_interfaces():
+ """
+ helper function to scan USB interfaces
+ returns a list of SuitableSerialInterface objects with name and id attributes
+ """
+ interfaces = []
+ for service in GetIOServicesByType('IOSerialBSDClient'):
+ device = get_string_property(service, "IOCalloutDevice")
+ if device:
+ usb_device = GetParentDeviceByType(service, "IOUSBInterface")
+ if usb_device:
+ name = get_string_property(usb_device, "USB Interface Name") or None
+ locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) or ''
+ i = SuitableSerialInterface()
+ i.id = locationID
+ i.name = name
+ interfaces.append(i)
+ return interfaces
+
+
+def search_for_locationID_in_interfaces(serial_interfaces, locationID):
+ for interface in serial_interfaces:
+ if (interface.id == locationID):
+ return interface.name
+ return None
+
+
+def comports(include_links=False):
+ # XXX include_links is currently ignored. are links in /dev even supported here?
+ # Scan for all iokit serial ports
+ services = GetIOServicesByType('IOSerialBSDClient')
+ ports = []
+ serial_interfaces = scan_interfaces()
+ for service in services:
+ # First, add the callout device file.
+ device = get_string_property(service, "IOCalloutDevice")
+ if device:
+ info = list_ports_common.ListPortInfo(device)
+ # If the serial port is implemented by IOUSBDevice
+ # NOTE IOUSBDevice was deprecated as of 10.11 and finally on Apple Silicon
+ # devices has been completely removed. Thanks to @oskay for this patch.
+ usb_device = GetParentDeviceByType(service, "IOUSBHostDevice")
+ if not usb_device:
+ usb_device = GetParentDeviceByType(service, "IOUSBDevice")
+ if usb_device:
+ # fetch some useful informations from properties
+ info.vid = get_int_property(usb_device, "idVendor", kCFNumberSInt16Type)
+ info.pid = get_int_property(usb_device, "idProduct", kCFNumberSInt16Type)
+ info.serial_number = get_string_property(usb_device, kUSBSerialNumberString)
+ # We know this is a usb device, so the
+ # IORegistryEntryName should always be aliased to the
+ # usb product name string descriptor.
+ info.product = IORegistryEntryGetName(usb_device) or 'n/a'
+ info.manufacturer = get_string_property(usb_device, kUSBVendorString)
+ locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type)
+ info.location = location_to_string(locationID)
+ info.interface = search_for_locationID_in_interfaces(serial_interfaces, locationID)
+ info.apply_usb_info()
+ ports.append(info)
+ return ports
+
+# test
+if __name__ == '__main__':
+ for port, desc, hwid in sorted(comports()):
+ print("{}: {} [{}]".format(port, desc, hwid))
diff --git a/mixly/tools/python/serial/tools/list_ports_posix.py b/mixly/tools/python/serial/tools/list_ports_posix.py
new file mode 100644
index 00000000..79bc8ed1
--- /dev/null
+++ b/mixly/tools/python/serial/tools/list_ports_posix.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+#
+# This is a module that gathers a list of serial ports on POSIXy systems.
+# For some specific implementations, see also list_ports_linux, list_ports_osx
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2011-2015 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""\
+The ``comports`` function is expected to return an iterable that yields tuples
+of 3 strings: port name, human readable description and a hardware ID.
+
+As currently no method is known to get the second two strings easily, they are
+currently just identical to the port name.
+"""
+
+from __future__ import absolute_import
+
+import glob
+import sys
+import os
+from serial.tools import list_ports_common
+
+# try to detect the OS so that a device can be selected...
+plat = sys.platform.lower()
+
+if plat[:5] == 'linux': # Linux (confirmed) # noqa
+ from serial.tools.list_ports_linux import comports
+
+elif plat[:6] == 'darwin': # OS X (confirmed)
+ from serial.tools.list_ports_osx import comports
+
+elif plat == 'cygwin': # cygwin/win32
+ # cygwin accepts /dev/com* in many contexts
+ # (such as 'open' call, explicit 'ls'), but 'glob.glob'
+ # and bare 'ls' do not; so use /dev/ttyS* instead
+ def comports(include_links=False):
+ devices = glob.glob('/dev/ttyS*')
+ if include_links:
+ devices.extend(list_ports_common.list_links(devices))
+ return [list_ports_common.ListPortInfo(d) for d in devices]
+
+elif plat[:7] == 'openbsd': # OpenBSD
+ def comports(include_links=False):
+ devices = glob.glob('/dev/cua*')
+ if include_links:
+ devices.extend(list_ports_common.list_links(devices))
+ return [list_ports_common.ListPortInfo(d) for d in devices]
+
+elif plat[:3] == 'bsd' or plat[:7] == 'freebsd':
+ def comports(include_links=False):
+ devices = glob.glob('/dev/cua*[!.init][!.lock]')
+ if include_links:
+ devices.extend(list_ports_common.list_links(devices))
+ return [list_ports_common.ListPortInfo(d) for d in devices]
+
+elif plat[:6] == 'netbsd': # NetBSD
+ def comports(include_links=False):
+ """scan for available ports. return a list of device names."""
+ devices = glob.glob('/dev/dty*')
+ if include_links:
+ devices.extend(list_ports_common.list_links(devices))
+ return [list_ports_common.ListPortInfo(d) for d in devices]
+
+elif plat[:4] == 'irix': # IRIX
+ def comports(include_links=False):
+ """scan for available ports. return a list of device names."""
+ devices = glob.glob('/dev/ttyf*')
+ if include_links:
+ devices.extend(list_ports_common.list_links(devices))
+ return [list_ports_common.ListPortInfo(d) for d in devices]
+
+elif plat[:2] == 'hp': # HP-UX (not tested)
+ def comports(include_links=False):
+ """scan for available ports. return a list of device names."""
+ devices = glob.glob('/dev/tty*p0')
+ if include_links:
+ devices.extend(list_ports_common.list_links(devices))
+ return [list_ports_common.ListPortInfo(d) for d in devices]
+
+elif plat[:5] == 'sunos': # Solaris/SunOS
+ def comports(include_links=False):
+ """scan for available ports. return a list of device names."""
+ devices = glob.glob('/dev/tty*c')
+ if include_links:
+ devices.extend(list_ports_common.list_links(devices))
+ return [list_ports_common.ListPortInfo(d) for d in devices]
+
+elif plat[:3] == 'aix': # AIX
+ def comports(include_links=False):
+ """scan for available ports. return a list of device names."""
+ devices = glob.glob('/dev/tty*')
+ if include_links:
+ devices.extend(list_ports_common.list_links(devices))
+ return [list_ports_common.ListPortInfo(d) for d in devices]
+
+else:
+ # platform detection has failed...
+ import serial
+ sys.stderr.write("""\
+don't know how to enumerate ttys on this system.
+! I you know how the serial ports are named send this information to
+! the author of this module:
+
+sys.platform = {!r}
+os.name = {!r}
+pySerial version = {}
+
+also add the naming scheme of the serial ports and with a bit luck you can get
+this module running...
+""".format(sys.platform, os.name, serial.VERSION))
+ raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
+
+# test
+if __name__ == '__main__':
+ for port, desc, hwid in sorted(comports()):
+ print("{}: {} [{}]".format(port, desc, hwid))
diff --git a/mixly/tools/python/serial/tools/list_ports_windows.py b/mixly/tools/python/serial/tools/list_ports_windows.py
new file mode 100644
index 00000000..0b4a5b1e
--- /dev/null
+++ b/mixly/tools/python/serial/tools/list_ports_windows.py
@@ -0,0 +1,427 @@
+#! python
+#
+# Enumerate serial ports on Windows including a human readable description
+# and hardware information.
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2001-2016 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import absolute_import
+
+# pylint: disable=invalid-name,too-few-public-methods
+import re
+import ctypes
+from ctypes.wintypes import BOOL
+from ctypes.wintypes import HWND
+from ctypes.wintypes import DWORD
+from ctypes.wintypes import WORD
+from ctypes.wintypes import LONG
+from ctypes.wintypes import ULONG
+from ctypes.wintypes import HKEY
+from ctypes.wintypes import BYTE
+import serial
+from serial.win32 import ULONG_PTR
+from serial.tools import list_ports_common
+
+
+def ValidHandle(value, func, arguments):
+ if value == 0:
+ raise ctypes.WinError()
+ return value
+
+
+NULL = 0
+HDEVINFO = ctypes.c_void_p
+LPCTSTR = ctypes.c_wchar_p
+PCTSTR = ctypes.c_wchar_p
+PTSTR = ctypes.c_wchar_p
+LPDWORD = PDWORD = ctypes.POINTER(DWORD)
+#~ LPBYTE = PBYTE = ctypes.POINTER(BYTE)
+LPBYTE = PBYTE = ctypes.c_void_p # XXX avoids error about types
+
+ACCESS_MASK = DWORD
+REGSAM = ACCESS_MASK
+
+
+class GUID(ctypes.Structure):
+ _fields_ = [
+ ('Data1', DWORD),
+ ('Data2', WORD),
+ ('Data3', WORD),
+ ('Data4', BYTE * 8),
+ ]
+
+ def __str__(self):
+ return "{{{:08x}-{:04x}-{:04x}-{}-{}}}".format(
+ self.Data1,
+ self.Data2,
+ self.Data3,
+ ''.join(["{:02x}".format(d) for d in self.Data4[:2]]),
+ ''.join(["{:02x}".format(d) for d in self.Data4[2:]]),
+ )
+
+
+class SP_DEVINFO_DATA(ctypes.Structure):
+ _fields_ = [
+ ('cbSize', DWORD),
+ ('ClassGuid', GUID),
+ ('DevInst', DWORD),
+ ('Reserved', ULONG_PTR),
+ ]
+
+ def __str__(self):
+ return "ClassGuid:{} DevInst:{}".format(self.ClassGuid, self.DevInst)
+
+
+PSP_DEVINFO_DATA = ctypes.POINTER(SP_DEVINFO_DATA)
+
+PSP_DEVICE_INTERFACE_DETAIL_DATA = ctypes.c_void_p
+
+setupapi = ctypes.windll.LoadLibrary("setupapi")
+SetupDiDestroyDeviceInfoList = setupapi.SetupDiDestroyDeviceInfoList
+SetupDiDestroyDeviceInfoList.argtypes = [HDEVINFO]
+SetupDiDestroyDeviceInfoList.restype = BOOL
+
+SetupDiClassGuidsFromName = setupapi.SetupDiClassGuidsFromNameW
+SetupDiClassGuidsFromName.argtypes = [PCTSTR, ctypes.POINTER(GUID), DWORD, PDWORD]
+SetupDiClassGuidsFromName.restype = BOOL
+
+SetupDiEnumDeviceInfo = setupapi.SetupDiEnumDeviceInfo
+SetupDiEnumDeviceInfo.argtypes = [HDEVINFO, DWORD, PSP_DEVINFO_DATA]
+SetupDiEnumDeviceInfo.restype = BOOL
+
+SetupDiGetClassDevs = setupapi.SetupDiGetClassDevsW
+SetupDiGetClassDevs.argtypes = [ctypes.POINTER(GUID), PCTSTR, HWND, DWORD]
+SetupDiGetClassDevs.restype = HDEVINFO
+SetupDiGetClassDevs.errcheck = ValidHandle
+
+SetupDiGetDeviceRegistryProperty = setupapi.SetupDiGetDeviceRegistryPropertyW
+SetupDiGetDeviceRegistryProperty.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, PDWORD, PBYTE, DWORD, PDWORD]
+SetupDiGetDeviceRegistryProperty.restype = BOOL
+
+SetupDiGetDeviceInstanceId = setupapi.SetupDiGetDeviceInstanceIdW
+SetupDiGetDeviceInstanceId.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, PTSTR, DWORD, PDWORD]
+SetupDiGetDeviceInstanceId.restype = BOOL
+
+SetupDiOpenDevRegKey = setupapi.SetupDiOpenDevRegKey
+SetupDiOpenDevRegKey.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, DWORD, DWORD, REGSAM]
+SetupDiOpenDevRegKey.restype = HKEY
+
+advapi32 = ctypes.windll.LoadLibrary("Advapi32")
+RegCloseKey = advapi32.RegCloseKey
+RegCloseKey.argtypes = [HKEY]
+RegCloseKey.restype = LONG
+
+RegQueryValueEx = advapi32.RegQueryValueExW
+RegQueryValueEx.argtypes = [HKEY, LPCTSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD]
+RegQueryValueEx.restype = LONG
+
+cfgmgr32 = ctypes.windll.LoadLibrary("Cfgmgr32")
+CM_Get_Parent = cfgmgr32.CM_Get_Parent
+CM_Get_Parent.argtypes = [PDWORD, DWORD, ULONG]
+CM_Get_Parent.restype = LONG
+
+CM_Get_Device_IDW = cfgmgr32.CM_Get_Device_IDW
+CM_Get_Device_IDW.argtypes = [DWORD, PTSTR, ULONG, ULONG]
+CM_Get_Device_IDW.restype = LONG
+
+CM_MapCrToWin32Err = cfgmgr32.CM_MapCrToWin32Err
+CM_MapCrToWin32Err.argtypes = [DWORD, DWORD]
+CM_MapCrToWin32Err.restype = DWORD
+
+
+DIGCF_PRESENT = 2
+DIGCF_DEVICEINTERFACE = 16
+INVALID_HANDLE_VALUE = 0
+ERROR_INSUFFICIENT_BUFFER = 122
+ERROR_NOT_FOUND = 1168
+SPDRP_HARDWAREID = 1
+SPDRP_FRIENDLYNAME = 12
+SPDRP_LOCATION_PATHS = 35
+SPDRP_MFG = 11
+DICS_FLAG_GLOBAL = 1
+DIREG_DEV = 0x00000001
+KEY_READ = 0x20019
+
+
+MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH = 5
+
+
+def get_parent_serial_number(child_devinst, child_vid, child_pid, depth=0, last_serial_number=None):
+ """ Get the serial number of the parent of a device.
+
+ Args:
+ child_devinst: The device instance handle to get the parent serial number of.
+ child_vid: The vendor ID of the child device.
+ child_pid: The product ID of the child device.
+ depth: The current iteration depth of the USB device tree.
+ """
+
+ # If the traversal depth is beyond the max, abandon attempting to find the serial number.
+ if depth > MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH:
+ return '' if not last_serial_number else last_serial_number
+
+ # Get the parent device instance.
+ devinst = DWORD()
+ ret = CM_Get_Parent(ctypes.byref(devinst), child_devinst, 0)
+
+ if ret:
+ win_error = CM_MapCrToWin32Err(DWORD(ret), DWORD(0))
+
+ # If there is no parent available, the child was the root device. We cannot traverse
+ # further.
+ if win_error == ERROR_NOT_FOUND:
+ return '' if not last_serial_number else last_serial_number
+
+ raise ctypes.WinError(win_error)
+
+ # Get the ID of the parent device and parse it for vendor ID, product ID, and serial number.
+ parentHardwareID = ctypes.create_unicode_buffer(250)
+
+ ret = CM_Get_Device_IDW(
+ devinst,
+ parentHardwareID,
+ ctypes.sizeof(parentHardwareID) - 1,
+ 0)
+
+ if ret:
+ raise ctypes.WinError(CM_MapCrToWin32Err(DWORD(ret), DWORD(0)))
+
+ parentHardwareID_str = parentHardwareID.value
+ m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?',
+ parentHardwareID_str,
+ re.I)
+
+ # return early if we have no matches (likely malformed serial, traversed too far)
+ if not m:
+ return '' if not last_serial_number else last_serial_number
+
+ vid = None
+ pid = None
+ serial_number = None
+ if m.group(1):
+ vid = int(m.group(1), 16)
+ if m.group(3):
+ pid = int(m.group(3), 16)
+ if m.group(7):
+ serial_number = m.group(7)
+
+ # store what we found as a fallback for malformed serial values up the chain
+ found_serial_number = serial_number
+
+ # Check that the USB serial number only contains alpha-numeric characters. It may be a windows
+ # device ID (ephemeral ID).
+ if serial_number and not re.match(r'^\w+$', serial_number):
+ serial_number = None
+
+ if not vid or not pid:
+ # If pid and vid are not available at this device level, continue to the parent.
+ return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number)
+
+ if pid != child_pid or vid != child_vid:
+ # If the VID or PID has changed, we are no longer looking at the same physical device. The
+ # serial number is unknown.
+ return '' if not last_serial_number else last_serial_number
+
+ # In this case, the vid and pid of the parent device are identical to the child. However, if
+ # there still isn't a serial number available, continue to the next parent.
+ if not serial_number:
+ return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number)
+
+ # Finally, the VID and PID are identical to the child and a serial number is present, so return
+ # it.
+ return serial_number
+
+
+def iterate_comports():
+ """Return a generator that yields descriptions for serial ports"""
+ PortsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough...
+ ports_guids_size = DWORD()
+ if not SetupDiClassGuidsFromName(
+ "Ports",
+ PortsGUIDs,
+ ctypes.sizeof(PortsGUIDs),
+ ctypes.byref(ports_guids_size)):
+ raise ctypes.WinError()
+
+ ModemsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough...
+ modems_guids_size = DWORD()
+ if not SetupDiClassGuidsFromName(
+ "Modem",
+ ModemsGUIDs,
+ ctypes.sizeof(ModemsGUIDs),
+ ctypes.byref(modems_guids_size)):
+ raise ctypes.WinError()
+
+ GUIDs = PortsGUIDs[:ports_guids_size.value] + ModemsGUIDs[:modems_guids_size.value]
+
+ # repeat for all possible GUIDs
+ for index in range(len(GUIDs)):
+ bInterfaceNumber = None
+ g_hdi = SetupDiGetClassDevs(
+ ctypes.byref(GUIDs[index]),
+ None,
+ NULL,
+ DIGCF_PRESENT) # was DIGCF_PRESENT|DIGCF_DEVICEINTERFACE which misses CDC ports
+
+ devinfo = SP_DEVINFO_DATA()
+ devinfo.cbSize = ctypes.sizeof(devinfo)
+ index = 0
+ while SetupDiEnumDeviceInfo(g_hdi, index, ctypes.byref(devinfo)):
+ index += 1
+
+ # get the real com port name
+ hkey = SetupDiOpenDevRegKey(
+ g_hdi,
+ ctypes.byref(devinfo),
+ DICS_FLAG_GLOBAL,
+ 0,
+ DIREG_DEV, # DIREG_DRV for SW info
+ KEY_READ)
+ port_name_buffer = ctypes.create_unicode_buffer(250)
+ port_name_length = ULONG(ctypes.sizeof(port_name_buffer))
+ RegQueryValueEx(
+ hkey,
+ "PortName",
+ None,
+ None,
+ ctypes.byref(port_name_buffer),
+ ctypes.byref(port_name_length))
+ RegCloseKey(hkey)
+
+ # unfortunately does this method also include parallel ports.
+ # we could check for names starting with COM or just exclude LPT
+ # and hope that other "unknown" names are serial ports...
+ if port_name_buffer.value.startswith('LPT'):
+ continue
+
+ # hardware ID
+ szHardwareID = ctypes.create_unicode_buffer(250)
+ # try to get ID that includes serial number
+ if not SetupDiGetDeviceInstanceId(
+ g_hdi,
+ ctypes.byref(devinfo),
+ #~ ctypes.byref(szHardwareID),
+ szHardwareID,
+ ctypes.sizeof(szHardwareID) - 1,
+ None):
+ # fall back to more generic hardware ID if that would fail
+ if not SetupDiGetDeviceRegistryProperty(
+ g_hdi,
+ ctypes.byref(devinfo),
+ SPDRP_HARDWAREID,
+ None,
+ ctypes.byref(szHardwareID),
+ ctypes.sizeof(szHardwareID) - 1,
+ None):
+ # Ignore ERROR_INSUFFICIENT_BUFFER
+ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
+ raise ctypes.WinError()
+ # stringify
+ szHardwareID_str = szHardwareID.value
+
+ info = list_ports_common.ListPortInfo(port_name_buffer.value, skip_link_detection=True)
+
+ # in case of USB, make a more readable string, similar to that form
+ # that we also generate on other platforms
+ if szHardwareID_str.startswith('USB'):
+ m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?', szHardwareID_str, re.I)
+ if m:
+ info.vid = int(m.group(1), 16)
+ if m.group(3):
+ info.pid = int(m.group(3), 16)
+ if m.group(5):
+ bInterfaceNumber = int(m.group(5))
+
+ # Check that the USB serial number only contains alpha-numeric characters. It
+ # may be a windows device ID (ephemeral ID) for composite devices.
+ if m.group(7) and re.match(r'^\w+$', m.group(7)):
+ info.serial_number = m.group(7)
+ else:
+ info.serial_number = get_parent_serial_number(devinfo.DevInst, info.vid, info.pid)
+
+ # calculate a location string
+ loc_path_str = ctypes.create_unicode_buffer(250)
+ if SetupDiGetDeviceRegistryProperty(
+ g_hdi,
+ ctypes.byref(devinfo),
+ SPDRP_LOCATION_PATHS,
+ None,
+ ctypes.byref(loc_path_str),
+ ctypes.sizeof(loc_path_str) - 1,
+ None):
+ m = re.finditer(r'USBROOT\((\w+)\)|#USB\((\w+)\)', loc_path_str.value)
+ location = []
+ for g in m:
+ if g.group(1):
+ location.append('{:d}'.format(int(g.group(1)) + 1))
+ else:
+ if len(location) > 1:
+ location.append('.')
+ else:
+ location.append('-')
+ location.append(g.group(2))
+ if bInterfaceNumber is not None:
+ location.append(':{}.{}'.format(
+ 'x', # XXX how to determine correct bConfigurationValue?
+ bInterfaceNumber))
+ if location:
+ info.location = ''.join(location)
+ info.hwid = info.usb_info()
+ elif szHardwareID_str.startswith('FTDIBUS'):
+ m = re.search(r'VID_([0-9a-f]{4})\+PID_([0-9a-f]{4})(\+(\w+))?', szHardwareID_str, re.I)
+ if m:
+ info.vid = int(m.group(1), 16)
+ info.pid = int(m.group(2), 16)
+ if m.group(4):
+ info.serial_number = m.group(4)
+ # USB location is hidden by FDTI driver :(
+ info.hwid = info.usb_info()
+ else:
+ info.hwid = szHardwareID_str
+
+ # friendly name
+ szFriendlyName = ctypes.create_unicode_buffer(250)
+ if SetupDiGetDeviceRegistryProperty(
+ g_hdi,
+ ctypes.byref(devinfo),
+ SPDRP_FRIENDLYNAME,
+ #~ SPDRP_DEVICEDESC,
+ None,
+ ctypes.byref(szFriendlyName),
+ ctypes.sizeof(szFriendlyName) - 1,
+ None):
+ info.description = szFriendlyName.value
+ #~ else:
+ # Ignore ERROR_INSUFFICIENT_BUFFER
+ #~ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
+ #~ raise IOError("failed to get details for %s (%s)" % (devinfo, szHardwareID.value))
+ # ignore errors and still include the port in the list, friendly name will be same as port name
+
+ # manufacturer
+ szManufacturer = ctypes.create_unicode_buffer(250)
+ if SetupDiGetDeviceRegistryProperty(
+ g_hdi,
+ ctypes.byref(devinfo),
+ SPDRP_MFG,
+ #~ SPDRP_DEVICEDESC,
+ None,
+ ctypes.byref(szManufacturer),
+ ctypes.sizeof(szManufacturer) - 1,
+ None):
+ info.manufacturer = szManufacturer.value
+ yield info
+ SetupDiDestroyDeviceInfoList(g_hdi)
+
+
+def comports(include_links=False):
+ """Return a list of info objects about serial ports"""
+ return list(iterate_comports())
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# test
+if __name__ == '__main__':
+ for port, desc, hwid in sorted(comports()):
+ print("{}: {} [{}]".format(port, desc, hwid))
diff --git a/mixly/tools/python/serial/tools/miniterm.py b/mixly/tools/python/serial/tools/miniterm.py
new file mode 100644
index 00000000..2cceff63
--- /dev/null
+++ b/mixly/tools/python/serial/tools/miniterm.py
@@ -0,0 +1,1042 @@
+#!/usr/bin/env python
+#
+# Very simple serial terminal
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C)2002-2020 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import absolute_import
+
+import codecs
+import os
+import sys
+import threading
+
+import serial
+from serial.tools.list_ports import comports
+from serial.tools import hexlify_codec
+
+# pylint: disable=wrong-import-order,wrong-import-position
+
+codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None)
+
+try:
+ raw_input
+except NameError:
+ # pylint: disable=redefined-builtin,invalid-name
+ raw_input = input # in python3 it's "raw"
+ unichr = chr
+
+
+def key_description(character):
+ """generate a readable description for a key"""
+ ascii_code = ord(character)
+ if ascii_code < 32:
+ return 'Ctrl+{:c}'.format(ord('@') + ascii_code)
+ else:
+ return repr(character)
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+class ConsoleBase(object):
+ """OS abstraction for console (input/output codec, no echo)"""
+
+ def __init__(self):
+ if sys.version_info >= (3, 0):
+ self.byte_output = sys.stdout.buffer
+ else:
+ self.byte_output = sys.stdout
+ self.output = sys.stdout
+
+ def setup(self):
+ """Set console to read single characters, no echo"""
+
+ def cleanup(self):
+ """Restore default console settings"""
+
+ def getkey(self):
+ """Read a single key from the console"""
+ return None
+
+ def write_bytes(self, byte_string):
+ """Write bytes (already encoded)"""
+ self.byte_output.write(byte_string)
+ self.byte_output.flush()
+
+ def write(self, text):
+ """Write string"""
+ self.output.write(text)
+ self.output.flush()
+
+ def cancel(self):
+ """Cancel getkey operation"""
+
+ # - - - - - - - - - - - - - - - - - - - - - - - -
+ # context manager:
+ # switch terminal temporary to normal mode (e.g. to get user input)
+
+ def __enter__(self):
+ self.cleanup()
+ return self
+
+ def __exit__(self, *args, **kwargs):
+ self.setup()
+
+
+if os.name == 'nt': # noqa
+ import msvcrt
+ import ctypes
+ import platform
+
+ class Out(object):
+ """file-like wrapper that uses os.write"""
+
+ def __init__(self, fd):
+ self.fd = fd
+
+ def flush(self):
+ pass
+
+ def write(self, s):
+ os.write(self.fd, s)
+
+ class Console(ConsoleBase):
+ fncodes = {
+ ';': '\1bOP', # F1
+ '<': '\1bOQ', # F2
+ '=': '\1bOR', # F3
+ '>': '\1bOS', # F4
+ '?': '\1b[15~', # F5
+ '@': '\1b[17~', # F6
+ 'A': '\1b[18~', # F7
+ 'B': '\1b[19~', # F8
+ 'C': '\1b[20~', # F9
+ 'D': '\1b[21~', # F10
+ }
+ navcodes = {
+ 'H': '\x1b[A', # UP
+ 'P': '\x1b[B', # DOWN
+ 'K': '\x1b[D', # LEFT
+ 'M': '\x1b[C', # RIGHT
+ 'G': '\x1b[H', # HOME
+ 'O': '\x1b[F', # END
+ 'R': '\x1b[2~', # INSERT
+ 'S': '\x1b[3~', # DELETE
+ 'I': '\x1b[5~', # PGUP
+ 'Q': '\x1b[6~', # PGDN
+ }
+
+ def __init__(self):
+ super(Console, self).__init__()
+ self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP()
+ self._saved_icp = ctypes.windll.kernel32.GetConsoleCP()
+ ctypes.windll.kernel32.SetConsoleOutputCP(65001)
+ ctypes.windll.kernel32.SetConsoleCP(65001)
+ # ANSI handling available through SetConsoleMode since Windows 10 v1511
+ # https://en.wikipedia.org/wiki/ANSI_escape_code#cite_note-win10th2-1
+ if platform.release() == '10' and int(platform.version().split('.')[2]) > 10586:
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
+ import ctypes.wintypes as wintypes
+ if not hasattr(wintypes, 'LPDWORD'): # PY2
+ wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)
+ SetConsoleMode = ctypes.windll.kernel32.SetConsoleMode
+ GetConsoleMode = ctypes.windll.kernel32.GetConsoleMode
+ GetStdHandle = ctypes.windll.kernel32.GetStdHandle
+ mode = wintypes.DWORD()
+ GetConsoleMode(GetStdHandle(-11), ctypes.byref(mode))
+ if (mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0:
+ SetConsoleMode(GetStdHandle(-11), mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+ self._saved_cm = mode
+ self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace')
+ # the change of the code page is not propagated to Python, manually fix it
+ sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace')
+ sys.stdout = self.output
+ self.output.encoding = 'UTF-8' # needed for input
+
+ def __del__(self):
+ ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp)
+ ctypes.windll.kernel32.SetConsoleCP(self._saved_icp)
+ try:
+ ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), self._saved_cm)
+ except AttributeError: # in case no _saved_cm
+ pass
+
+ def getkey(self):
+ while True:
+ z = msvcrt.getwch()
+ if z == unichr(13):
+ return unichr(10)
+ elif z is unichr(0) or z is unichr(0xe0):
+ try:
+ code = msvcrt.getwch()
+ if z is unichr(0):
+ return self.fncodes[code]
+ else:
+ return self.navcodes[code]
+ except KeyError:
+ pass
+ else:
+ return z
+
+ def cancel(self):
+ # CancelIo, CancelSynchronousIo do not seem to work when using
+ # getwch, so instead, send a key to the window with the console
+ hwnd = ctypes.windll.kernel32.GetConsoleWindow()
+ ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0)
+
+elif os.name == 'posix':
+ import atexit
+ import termios
+ import fcntl
+
+ class Console(ConsoleBase):
+ def __init__(self):
+ super(Console, self).__init__()
+ self.fd = sys.stdin.fileno()
+ self.old = termios.tcgetattr(self.fd)
+ atexit.register(self.cleanup)
+ if sys.version_info < (3, 0):
+ self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin)
+ else:
+ self.enc_stdin = sys.stdin
+
+ def setup(self):
+ new = termios.tcgetattr(self.fd)
+ new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
+ new[6][termios.VMIN] = 1
+ new[6][termios.VTIME] = 0
+ termios.tcsetattr(self.fd, termios.TCSANOW, new)
+
+ def getkey(self):
+ c = self.enc_stdin.read(1)
+ if c == unichr(0x7f):
+ c = unichr(8) # map the BS key (which yields DEL) to backspace
+ return c
+
+ def cancel(self):
+ fcntl.ioctl(self.fd, termios.TIOCSTI, b'\0')
+
+ def cleanup(self):
+ termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
+
+else:
+ raise NotImplementedError(
+ 'Sorry no implementation for your platform ({}) available.'.format(sys.platform))
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+class Transform(object):
+ """do-nothing: forward all data unchanged"""
+ def rx(self, text):
+ """text received from serial port"""
+ return text
+
+ def tx(self, text):
+ """text to be sent to serial port"""
+ return text
+
+ def echo(self, text):
+ """text to be sent but displayed on console"""
+ return text
+
+
+class CRLF(Transform):
+ """ENTER sends CR+LF"""
+
+ def tx(self, text):
+ return text.replace('\n', '\r\n')
+
+
+class CR(Transform):
+ """ENTER sends CR"""
+
+ def rx(self, text):
+ return text.replace('\r', '\n')
+
+ def tx(self, text):
+ return text.replace('\n', '\r')
+
+
+class LF(Transform):
+ """ENTER sends LF"""
+
+
+class NoTerminal(Transform):
+ """remove typical terminal control codes from input"""
+
+ REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32) if unichr(x) not in '\r\n\b\t')
+ REPLACEMENT_MAP.update(
+ {
+ 0x7F: 0x2421, # DEL
+ 0x9B: 0x2425, # CSI
+ })
+
+ def rx(self, text):
+ return text.translate(self.REPLACEMENT_MAP)
+
+ echo = rx
+
+
+class NoControls(NoTerminal):
+ """Remove all control codes, incl. CR+LF"""
+
+ REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32))
+ REPLACEMENT_MAP.update(
+ {
+ 0x20: 0x2423, # visual space
+ 0x7F: 0x2421, # DEL
+ 0x9B: 0x2425, # CSI
+ })
+
+
+class Printable(Transform):
+ """Show decimal code for all non-ASCII characters and replace most control codes"""
+
+ def rx(self, text):
+ r = []
+ for c in text:
+ if ' ' <= c < '\x7f' or c in '\r\n\b\t':
+ r.append(c)
+ elif c < ' ':
+ r.append(unichr(0x2400 + ord(c)))
+ else:
+ r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c)))
+ r.append(' ')
+ return ''.join(r)
+
+ echo = rx
+
+
+class Colorize(Transform):
+ """Apply different colors for received and echo"""
+
+ def __init__(self):
+ # XXX make it configurable, use colorama?
+ self.input_color = '\x1b[37m'
+ self.echo_color = '\x1b[31m'
+
+ def rx(self, text):
+ return self.input_color + text
+
+ def echo(self, text):
+ return self.echo_color + text
+
+
+class DebugIO(Transform):
+ """Print what is sent and received"""
+
+ def rx(self, text):
+ sys.stderr.write(' [RX:{!r}] '.format(text))
+ sys.stderr.flush()
+ return text
+
+ def tx(self, text):
+ sys.stderr.write(' [TX:{!r}] '.format(text))
+ sys.stderr.flush()
+ return text
+
+
+# other ideas:
+# - add date/time for each newline
+# - insert newline after: a) timeout b) packet end character
+
+EOL_TRANSFORMATIONS = {
+ 'crlf': CRLF,
+ 'cr': CR,
+ 'lf': LF,
+}
+
+TRANSFORMATIONS = {
+ 'direct': Transform, # no transformation
+ 'default': NoTerminal,
+ 'nocontrol': NoControls,
+ 'printable': Printable,
+ 'colorize': Colorize,
+ 'debug': DebugIO,
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+def ask_for_port():
+ """\
+ Show a list of ports and ask the user for a choice. To make selection
+ easier on systems with long device names, also allow the input of an
+ index.
+ """
+ sys.stderr.write('\n--- Available ports:\n')
+ ports = []
+ for n, (port, desc, hwid) in enumerate(sorted(comports()), 1):
+ sys.stderr.write('--- {:2}: {:20} {!r}\n'.format(n, port, desc))
+ ports.append(port)
+ while True:
+ port = raw_input('--- Enter port index or full name: ')
+ try:
+ index = int(port) - 1
+ if not 0 <= index < len(ports):
+ sys.stderr.write('--- Invalid index!\n')
+ continue
+ except ValueError:
+ pass
+ else:
+ port = ports[index]
+ return port
+
+
+class Miniterm(object):
+ """\
+ Terminal application. Copy data from serial port to console and vice versa.
+ Handle special keys from the console to show menu etc.
+ """
+
+ def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
+ self.console = Console()
+ self.serial = serial_instance
+ self.echo = echo
+ self.raw = False
+ self.input_encoding = 'UTF-8'
+ self.output_encoding = 'UTF-8'
+ self.eol = eol
+ self.filters = filters
+ self.update_transformations()
+ self.exit_character = unichr(0x1d) # GS/CTRL+]
+ self.menu_character = unichr(0x14) # Menu: CTRL+T
+ self.alive = None
+ self._reader_alive = None
+ self.receiver_thread = None
+ self.rx_decoder = None
+ self.tx_decoder = None
+
+ def _start_reader(self):
+ """Start reader thread"""
+ self._reader_alive = True
+ # start serial->console thread
+ self.receiver_thread = threading.Thread(target=self.reader, name='rx')
+ self.receiver_thread.daemon = True
+ self.receiver_thread.start()
+
+ def _stop_reader(self):
+ """Stop reader thread only, wait for clean exit of thread"""
+ self._reader_alive = False
+ if hasattr(self.serial, 'cancel_read'):
+ self.serial.cancel_read()
+ self.receiver_thread.join()
+
+ def start(self):
+ """start worker threads"""
+ self.alive = True
+ self._start_reader()
+ # enter console->serial loop
+ self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
+ self.transmitter_thread.daemon = True
+ self.transmitter_thread.start()
+ self.console.setup()
+
+ def stop(self):
+ """set flag to stop worker threads"""
+ self.alive = False
+
+ def join(self, transmit_only=False):
+ """wait for worker threads to terminate"""
+ self.transmitter_thread.join()
+ if not transmit_only:
+ if hasattr(self.serial, 'cancel_read'):
+ self.serial.cancel_read()
+ self.receiver_thread.join()
+
+ def close(self):
+ self.serial.close()
+
+ def update_transformations(self):
+ """take list of transformation classes and instantiate them for rx and tx"""
+ transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f]
+ for f in self.filters]
+ self.tx_transformations = [t() for t in transformations]
+ self.rx_transformations = list(reversed(self.tx_transformations))
+
+ def set_rx_encoding(self, encoding, errors='replace'):
+ """set encoding for received data"""
+ self.input_encoding = encoding
+ self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
+
+ def set_tx_encoding(self, encoding, errors='replace'):
+ """set encoding for transmitted data"""
+ self.output_encoding = encoding
+ self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
+
+ def dump_port_settings(self):
+ """Write current settings to sys.stderr"""
+ sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
+ p=self.serial))
+ sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
+ ('active' if self.serial.rts else 'inactive'),
+ ('active' if self.serial.dtr else 'inactive'),
+ ('active' if self.serial.break_condition else 'inactive')))
+ try:
+ sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
+ ('active' if self.serial.cts else 'inactive'),
+ ('active' if self.serial.dsr else 'inactive'),
+ ('active' if self.serial.ri else 'inactive'),
+ ('active' if self.serial.cd else 'inactive')))
+ except serial.SerialException:
+ # on RFC 2217 ports, it can happen if no modem state notification was
+ # yet received. ignore this error.
+ pass
+ sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
+ sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
+ sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
+ sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
+ sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
+ sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
+
+ def reader(self):
+ """loop and copy serial->console"""
+ try:
+ while self.alive and self._reader_alive:
+ # read all that is there or wait for one byte
+ data = self.serial.read(self.serial.in_waiting or 1)
+ if data:
+ if self.raw:
+ self.console.write_bytes(data)
+ else:
+ text = self.rx_decoder.decode(data)
+ for transformation in self.rx_transformations:
+ text = transformation.rx(text)
+ self.console.write(text)
+ except serial.SerialException:
+ self.alive = False
+ self.console.cancel()
+ raise # XXX handle instead of re-raise?
+
+ def writer(self):
+ """\
+ Loop and copy console->serial until self.exit_character character is
+ found. When self.menu_character is found, interpret the next key
+ locally.
+ """
+ menu_active = False
+ try:
+ while self.alive:
+ try:
+ c = self.console.getkey()
+ except KeyboardInterrupt:
+ c = '\x03'
+ if not self.alive:
+ break
+ if menu_active:
+ self.handle_menu_key(c)
+ menu_active = False
+ elif c == self.menu_character:
+ menu_active = True # next char will be for menu
+ elif c == self.exit_character:
+ self.stop() # exit app
+ break
+ else:
+ #~ if self.raw:
+ text = c
+ for transformation in self.tx_transformations:
+ text = transformation.tx(text)
+ self.serial.write(self.tx_encoder.encode(text))
+ if self.echo:
+ echo_text = c
+ for transformation in self.tx_transformations:
+ echo_text = transformation.echo(echo_text)
+ self.console.write(echo_text)
+ except:
+ self.alive = False
+ raise
+
+ def handle_menu_key(self, c):
+ """Implement a simple menu / settings"""
+ if c == self.menu_character or c == self.exit_character:
+ # Menu/exit character again -> send itself
+ self.serial.write(self.tx_encoder.encode(c))
+ if self.echo:
+ self.console.write(c)
+ elif c == '\x15': # CTRL+U -> upload file
+ self.upload_file()
+ elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
+ sys.stderr.write(self.get_help_text())
+ elif c == '\x12': # CTRL+R -> Toggle RTS
+ self.serial.rts = not self.serial.rts
+ sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
+ elif c == '\x04': # CTRL+D -> Toggle DTR
+ self.serial.dtr = not self.serial.dtr
+ sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
+ elif c == '\x02': # CTRL+B -> toggle BREAK condition
+ self.serial.break_condition = not self.serial.break_condition
+ sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
+ elif c == '\x05': # CTRL+E -> toggle local echo
+ self.echo = not self.echo
+ sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
+ elif c == '\x06': # CTRL+F -> edit filters
+ self.change_filter()
+ elif c == '\x0c': # CTRL+L -> EOL mode
+ modes = list(EOL_TRANSFORMATIONS) # keys
+ eol = modes.index(self.eol) + 1
+ if eol >= len(modes):
+ eol = 0
+ self.eol = modes[eol]
+ sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
+ self.update_transformations()
+ elif c == '\x01': # CTRL+A -> set encoding
+ self.change_encoding()
+ elif c == '\x09': # CTRL+I -> info
+ self.dump_port_settings()
+ #~ elif c == '\x01': # CTRL+A -> cycle escape mode
+ #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
+ elif c in 'pP': # P -> change port
+ self.change_port()
+ elif c in 'zZ': # S -> suspend / open port temporarily
+ self.suspend_port()
+ elif c in 'bB': # B -> change baudrate
+ self.change_baudrate()
+ elif c == '8': # 8 -> change to 8 bits
+ self.serial.bytesize = serial.EIGHTBITS
+ self.dump_port_settings()
+ elif c == '7': # 7 -> change to 8 bits
+ self.serial.bytesize = serial.SEVENBITS
+ self.dump_port_settings()
+ elif c in 'eE': # E -> change to even parity
+ self.serial.parity = serial.PARITY_EVEN
+ self.dump_port_settings()
+ elif c in 'oO': # O -> change to odd parity
+ self.serial.parity = serial.PARITY_ODD
+ self.dump_port_settings()
+ elif c in 'mM': # M -> change to mark parity
+ self.serial.parity = serial.PARITY_MARK
+ self.dump_port_settings()
+ elif c in 'sS': # S -> change to space parity
+ self.serial.parity = serial.PARITY_SPACE
+ self.dump_port_settings()
+ elif c in 'nN': # N -> change to no parity
+ self.serial.parity = serial.PARITY_NONE
+ self.dump_port_settings()
+ elif c == '1': # 1 -> change to 1 stop bits
+ self.serial.stopbits = serial.STOPBITS_ONE
+ self.dump_port_settings()
+ elif c == '2': # 2 -> change to 2 stop bits
+ self.serial.stopbits = serial.STOPBITS_TWO
+ self.dump_port_settings()
+ elif c == '3': # 3 -> change to 1.5 stop bits
+ self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
+ self.dump_port_settings()
+ elif c in 'xX': # X -> change software flow control
+ self.serial.xonxoff = (c == 'X')
+ self.dump_port_settings()
+ elif c in 'rR': # R -> change hardware flow control
+ self.serial.rtscts = (c == 'R')
+ self.dump_port_settings()
+ elif c in 'qQ':
+ self.stop() # Q -> exit app
+ else:
+ sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
+
+ def upload_file(self):
+ """Ask user for filenname and send its contents"""
+ sys.stderr.write('\n--- File to upload: ')
+ sys.stderr.flush()
+ with self.console:
+ filename = sys.stdin.readline().rstrip('\r\n')
+ if filename:
+ try:
+ with open(filename, 'rb') as f:
+ sys.stderr.write('--- Sending file {} ---\n'.format(filename))
+ while True:
+ block = f.read(1024)
+ if not block:
+ break
+ self.serial.write(block)
+ # Wait for output buffer to drain.
+ self.serial.flush()
+ sys.stderr.write('.') # Progress indicator.
+ sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
+ except IOError as e:
+ sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
+
+ def change_filter(self):
+ """change the i/o transformations"""
+ sys.stderr.write('\n--- Available Filters:\n')
+ sys.stderr.write('\n'.join(
+ '--- {:<10} = {.__doc__}'.format(k, v)
+ for k, v in sorted(TRANSFORMATIONS.items())))
+ sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
+ with self.console:
+ new_filters = sys.stdin.readline().lower().split()
+ if new_filters:
+ for f in new_filters:
+ if f not in TRANSFORMATIONS:
+ sys.stderr.write('--- unknown filter: {!r}\n'.format(f))
+ break
+ else:
+ self.filters = new_filters
+ self.update_transformations()
+ sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
+
+ def change_encoding(self):
+ """change encoding on the serial port"""
+ sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
+ with self.console:
+ new_encoding = sys.stdin.readline().strip()
+ if new_encoding:
+ try:
+ codecs.lookup(new_encoding)
+ except LookupError:
+ sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
+ else:
+ self.set_rx_encoding(new_encoding)
+ self.set_tx_encoding(new_encoding)
+ sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
+ sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
+
+ def change_baudrate(self):
+ """change the baudrate"""
+ sys.stderr.write('\n--- Baudrate: ')
+ sys.stderr.flush()
+ with self.console:
+ backup = self.serial.baudrate
+ try:
+ self.serial.baudrate = int(sys.stdin.readline().strip())
+ except ValueError as e:
+ sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
+ self.serial.baudrate = backup
+ else:
+ self.dump_port_settings()
+
+ def change_port(self):
+ """Have a conversation with the user to change the serial port"""
+ with self.console:
+ try:
+ port = ask_for_port()
+ except KeyboardInterrupt:
+ port = None
+ if port and port != self.serial.port:
+ # reader thread needs to be shut down
+ self._stop_reader()
+ # save settings
+ settings = self.serial.getSettingsDict()
+ try:
+ new_serial = serial.serial_for_url(port, do_not_open=True)
+ # restore settings and open
+ new_serial.applySettingsDict(settings)
+ new_serial.rts = self.serial.rts
+ new_serial.dtr = self.serial.dtr
+ new_serial.open()
+ new_serial.break_condition = self.serial.break_condition
+ except Exception as e:
+ sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
+ new_serial.close()
+ else:
+ self.serial.close()
+ self.serial = new_serial
+ sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
+ # and restart the reader thread
+ self._start_reader()
+
+ def suspend_port(self):
+ """\
+ open port temporarily, allow reconnect, exit and port change to get
+ out of the loop
+ """
+ # reader thread needs to be shut down
+ self._stop_reader()
+ self.serial.close()
+ sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port))
+ do_change_port = False
+ while not self.serial.is_open:
+ sys.stderr.write('--- Quit: {exit} | p: port change | any other key to reconnect ---\n'.format(
+ exit=key_description(self.exit_character)))
+ k = self.console.getkey()
+ if k == self.exit_character:
+ self.stop() # exit app
+ break
+ elif k in 'pP':
+ do_change_port = True
+ break
+ try:
+ self.serial.open()
+ except Exception as e:
+ sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e))
+ if do_change_port:
+ self.change_port()
+ else:
+ # and restart the reader thread
+ self._start_reader()
+ sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port))
+
+ def get_help_text(self):
+ """return the help text"""
+ # help text, starts with blank line!
+ return """
+--- pySerial ({version}) - miniterm - help
+---
+--- {exit:8} Exit program (alias {menu} Q)
+--- {menu:8} Menu escape key, followed by:
+--- Menu keys:
+--- {menu:7} Send the menu character itself to remote
+--- {exit:7} Send the exit character itself to remote
+--- {info:7} Show info
+--- {upload:7} Upload file (prompt will be shown)
+--- {repr:7} encoding
+--- {filter:7} edit filters
+--- Toggles:
+--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
+--- {echo:7} echo {eol:7} EOL
+---
+--- Port settings ({menu} followed by the following):
+--- p change port
+--- 7 8 set data bits
+--- N E O S M change parity (None, Even, Odd, Space, Mark)
+--- 1 2 3 set stop bits (1, 2, 1.5)
+--- b change baud rate
+--- x X disable/enable software flow control
+--- r R disable/enable hardware flow control
+""".format(version=getattr(serial, 'VERSION', 'unknown version'),
+ exit=key_description(self.exit_character),
+ menu=key_description(self.menu_character),
+ rts=key_description('\x12'),
+ dtr=key_description('\x04'),
+ brk=key_description('\x02'),
+ echo=key_description('\x05'),
+ info=key_description('\x09'),
+ upload=key_description('\x15'),
+ repr=key_description('\x01'),
+ filter=key_description('\x06'),
+ eol=key_description('\x0c'))
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# default args can be used to override when calling main() from an other script
+# e.g to create a miniterm-my-device.py
+def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
+ """Command line tool, entry point"""
+
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ description='Miniterm - A simple terminal program for the serial port.')
+
+ parser.add_argument(
+ 'port',
+ nargs='?',
+ help='serial port name ("-" to show port list)',
+ default=default_port)
+
+ parser.add_argument(
+ 'baudrate',
+ nargs='?',
+ type=int,
+ help='set baud rate, default: %(default)s',
+ default=default_baudrate)
+
+ group = parser.add_argument_group('port settings')
+
+ group.add_argument(
+ '--parity',
+ choices=['N', 'E', 'O', 'S', 'M'],
+ type=lambda c: c.upper(),
+ help='set parity, one of {N E O S M}, default: N',
+ default='N')
+
+ group.add_argument(
+ '--rtscts',
+ action='store_true',
+ help='enable RTS/CTS flow control (default off)',
+ default=False)
+
+ group.add_argument(
+ '--xonxoff',
+ action='store_true',
+ help='enable software flow control (default off)',
+ default=False)
+
+ group.add_argument(
+ '--rts',
+ type=int,
+ help='set initial RTS line state (possible values: 0, 1)',
+ default=default_rts)
+
+ group.add_argument(
+ '--dtr',
+ type=int,
+ help='set initial DTR line state (possible values: 0, 1)',
+ default=default_dtr)
+
+ group.add_argument(
+ '--non-exclusive',
+ dest='exclusive',
+ action='store_false',
+ help='disable locking for native ports',
+ default=True)
+
+ group.add_argument(
+ '--ask',
+ action='store_true',
+ help='ask again for port when open fails',
+ default=False)
+
+ group = parser.add_argument_group('data handling')
+
+ group.add_argument(
+ '-e', '--echo',
+ action='store_true',
+ help='enable local echo (default off)',
+ default=False)
+
+ group.add_argument(
+ '--encoding',
+ dest='serial_port_encoding',
+ metavar='CODEC',
+ help='set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s',
+ default='UTF-8')
+
+ group.add_argument(
+ '-f', '--filter',
+ action='append',
+ metavar='NAME',
+ help='add text transformation',
+ default=[])
+
+ group.add_argument(
+ '--eol',
+ choices=['CR', 'LF', 'CRLF'],
+ type=lambda c: c.upper(),
+ help='end of line mode',
+ default='CRLF')
+
+ group.add_argument(
+ '--raw',
+ action='store_true',
+ help='Do no apply any encodings/transformations',
+ default=False)
+
+ group = parser.add_argument_group('hotkeys')
+
+ group.add_argument(
+ '--exit-char',
+ type=int,
+ metavar='NUM',
+ help='Unicode of special character that is used to exit the application, default: %(default)s',
+ default=0x1d) # GS/CTRL+]
+
+ group.add_argument(
+ '--menu-char',
+ type=int,
+ metavar='NUM',
+ help='Unicode code of special character that is used to control miniterm (menu), default: %(default)s',
+ default=0x14) # Menu: CTRL+T
+
+ group = parser.add_argument_group('diagnostics')
+
+ group.add_argument(
+ '-q', '--quiet',
+ action='store_true',
+ help='suppress non-error messages',
+ default=False)
+
+ group.add_argument(
+ '--develop',
+ action='store_true',
+ help='show Python traceback on error',
+ default=False)
+
+ args = parser.parse_args()
+
+ if args.menu_char == args.exit_char:
+ parser.error('--exit-char can not be the same as --menu-char')
+
+ if args.filter:
+ if 'help' in args.filter:
+ sys.stderr.write('Available filters:\n')
+ sys.stderr.write('\n'.join(
+ '{:<10} = {.__doc__}'.format(k, v)
+ for k, v in sorted(TRANSFORMATIONS.items())))
+ sys.stderr.write('\n')
+ sys.exit(1)
+ filters = args.filter
+ else:
+ filters = ['default']
+
+ while True:
+ # no port given on command line -> ask user now
+ if args.port is None or args.port == '-':
+ try:
+ args.port = ask_for_port()
+ except KeyboardInterrupt:
+ sys.stderr.write('\n')
+ parser.error('user aborted and port is not given')
+ else:
+ if not args.port:
+ parser.error('port is not given')
+ try:
+ serial_instance = serial.serial_for_url(
+ args.port,
+ args.baudrate,
+ parity=args.parity,
+ rtscts=args.rtscts,
+ xonxoff=args.xonxoff,
+ do_not_open=True)
+
+ if not hasattr(serial_instance, 'cancel_read'):
+ # enable timeout for alive flag polling if cancel_read is not available
+ serial_instance.timeout = 1
+
+ if args.dtr is not None:
+ if not args.quiet:
+ sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
+ serial_instance.dtr = args.dtr
+ if args.rts is not None:
+ if not args.quiet:
+ sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
+ serial_instance.rts = args.rts
+
+ if isinstance(serial_instance, serial.Serial):
+ serial_instance.exclusive = args.exclusive
+
+ serial_instance.open()
+ except serial.SerialException as e:
+ sys.stderr.write('could not open port {!r}: {}\n'.format(args.port, e))
+ if args.develop:
+ raise
+ if not args.ask:
+ sys.exit(1)
+ else:
+ args.port = '-'
+ else:
+ break
+
+ miniterm = Miniterm(
+ serial_instance,
+ echo=args.echo,
+ eol=args.eol.lower(),
+ filters=filters)
+ miniterm.exit_character = unichr(args.exit_char)
+ miniterm.menu_character = unichr(args.menu_char)
+ miniterm.raw = args.raw
+ miniterm.set_rx_encoding(args.serial_port_encoding)
+ miniterm.set_tx_encoding(args.serial_port_encoding)
+
+ if not args.quiet:
+ sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
+ p=miniterm.serial))
+ sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
+ key_description(miniterm.exit_character),
+ key_description(miniterm.menu_character),
+ key_description(miniterm.menu_character),
+ key_description('\x08')))
+
+ miniterm.start()
+ try:
+ miniterm.join(True)
+ except KeyboardInterrupt:
+ pass
+ if not args.quiet:
+ sys.stderr.write('\n--- exit ---\n')
+ miniterm.join()
+ miniterm.close()
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+if __name__ == '__main__':
+ main()
diff --git a/mixly/tools/python/serial/urlhandler/__init__.py b/mixly/tools/python/serial/urlhandler/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mixly/tools/python/serial/urlhandler/protocol_alt.py b/mixly/tools/python/serial/urlhandler/protocol_alt.py
new file mode 100644
index 00000000..2e666ca7
--- /dev/null
+++ b/mixly/tools/python/serial/urlhandler/protocol_alt.py
@@ -0,0 +1,57 @@
+#! python
+#
+# This module implements a special URL handler that allows selecting an
+# alternate implementation provided by some backends.
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2015 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# URL format: alt://port[?option[=value][&option[=value]]]
+# options:
+# - class=X used class named X instead of Serial
+#
+# example:
+# use poll based implementation on Posix (Linux):
+# python -m serial.tools.miniterm alt:///dev/ttyUSB0?class=PosixPollSerial
+
+from __future__ import absolute_import
+
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
+import serial
+
+
+def serial_class_for_url(url):
+ """extract host and port from an URL string"""
+ parts = urlparse.urlsplit(url)
+ if parts.scheme != 'alt':
+ raise serial.SerialException(
+ 'expected a string in the form "alt://port[?option[=value][&option[=value]]]": '
+ 'not starting with alt:// ({!r})'.format(parts.scheme))
+ class_name = 'Serial'
+ try:
+ for option, values in urlparse.parse_qs(parts.query, True).items():
+ if option == 'class':
+ class_name = values[0]
+ else:
+ raise ValueError('unknown option: {!r}'.format(option))
+ except ValueError as e:
+ raise serial.SerialException(
+ 'expected a string in the form '
+ '"alt://port[?option[=value][&option[=value]]]": {!r}'.format(e))
+ if not hasattr(serial, class_name):
+ raise ValueError('unknown class: {!r}'.format(class_name))
+ cls = getattr(serial, class_name)
+ if not issubclass(cls, serial.Serial):
+ raise ValueError('class {!r} is not an instance of Serial'.format(class_name))
+ return (''.join([parts.netloc, parts.path]), cls)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+if __name__ == '__main__':
+ s = serial.serial_for_url('alt:///dev/ttyS0?class=PosixPollSerial')
+ print(s)
diff --git a/mixly/tools/python/serial/urlhandler/protocol_cp2110.py b/mixly/tools/python/serial/urlhandler/protocol_cp2110.py
new file mode 100644
index 00000000..44ad4eb4
--- /dev/null
+++ b/mixly/tools/python/serial/urlhandler/protocol_cp2110.py
@@ -0,0 +1,258 @@
+#! python
+#
+# Backend for Silicon Labs CP2110/4 HID-to-UART devices.
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2001-2015 Chris Liechti
+# (C) 2019 Google LLC
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+# This backend implements support for HID-to-UART devices manufactured
+# by Silicon Labs and marketed as CP2110 and CP2114. The
+# implementation is (mostly) OS-independent and in userland. It relies
+# on cython-hidapi (https://github.com/trezor/cython-hidapi).
+
+# The HID-to-UART protocol implemented by CP2110/4 is described in the
+# AN434 document from Silicon Labs:
+# https://www.silabs.com/documents/public/application-notes/AN434-CP2110-4-Interface-Specification.pdf
+
+# TODO items:
+
+# - rtscts support is configured for hardware flow control, but the
+# signaling is missing (AN434 suggests this is done through GPIO).
+# - Cancelling reads and writes is not supported.
+# - Baudrate validation is not implemented, as it depends on model and configuration.
+
+import struct
+import threading
+
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
+try:
+ import Queue
+except ImportError:
+ import queue as Queue
+
+import hid # hidapi
+
+import serial
+from serial.serialutil import SerialBase, SerialException, PortNotOpenError, to_bytes, Timeout
+
+
+# Report IDs and related constant
+_REPORT_GETSET_UART_ENABLE = 0x41
+_DISABLE_UART = 0x00
+_ENABLE_UART = 0x01
+
+_REPORT_SET_PURGE_FIFOS = 0x43
+_PURGE_TX_FIFO = 0x01
+_PURGE_RX_FIFO = 0x02
+
+_REPORT_GETSET_UART_CONFIG = 0x50
+
+_REPORT_SET_TRANSMIT_LINE_BREAK = 0x51
+_REPORT_SET_STOP_LINE_BREAK = 0x52
+
+
+class Serial(SerialBase):
+ # This is not quite correct. AN343 specifies that the minimum
+ # baudrate is different between CP2110 and CP2114, and it's halved
+ # when using non-8-bit symbols.
+ BAUDRATES = (300, 375, 600, 1200, 1800, 2400, 4800, 9600, 19200,
+ 38400, 57600, 115200, 230400, 460800, 500000, 576000,
+ 921600, 1000000)
+
+ def __init__(self, *args, **kwargs):
+ self._hid_handle = None
+ self._read_buffer = None
+ self._thread = None
+ super(Serial, self).__init__(*args, **kwargs)
+
+ def open(self):
+ if self._port is None:
+ raise SerialException("Port must be configured before it can be used.")
+ if self.is_open:
+ raise SerialException("Port is already open.")
+
+ self._read_buffer = Queue.Queue()
+
+ self._hid_handle = hid.device()
+ try:
+ portpath = self.from_url(self.portstr)
+ self._hid_handle.open_path(portpath)
+ except OSError as msg:
+ raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg))
+
+ try:
+ self._reconfigure_port()
+ except:
+ try:
+ self._hid_handle.close()
+ except:
+ pass
+ self._hid_handle = None
+ raise
+ else:
+ self.is_open = True
+ self._thread = threading.Thread(target=self._hid_read_loop)
+ self._thread.setDaemon(True)
+ self._thread.setName('pySerial CP2110 reader thread for {}'.format(self._port))
+ self._thread.start()
+
+ def from_url(self, url):
+ parts = urlparse.urlsplit(url)
+ if parts.scheme != "cp2110":
+ raise SerialException(
+ 'expected a string in the forms '
+ '"cp2110:///dev/hidraw9" or "cp2110://0001:0023:00": '
+ 'not starting with cp2110:// {{!r}}'.format(parts.scheme))
+ if parts.netloc: # cp2100://BUS:DEVICE:ENDPOINT, for libusb
+ return parts.netloc.encode('utf-8')
+ return parts.path.encode('utf-8')
+
+ def close(self):
+ self.is_open = False
+ if self._thread:
+ self._thread.join(1) # read timeout is 0.1
+ self._thread = None
+ self._hid_handle.close()
+ self._hid_handle = None
+
+ def _reconfigure_port(self):
+ parity_value = None
+ if self._parity == serial.PARITY_NONE:
+ parity_value = 0x00
+ elif self._parity == serial.PARITY_ODD:
+ parity_value = 0x01
+ elif self._parity == serial.PARITY_EVEN:
+ parity_value = 0x02
+ elif self._parity == serial.PARITY_MARK:
+ parity_value = 0x03
+ elif self._parity == serial.PARITY_SPACE:
+ parity_value = 0x04
+ else:
+ raise ValueError('Invalid parity: {!r}'.format(self._parity))
+
+ if self.rtscts:
+ flow_control_value = 0x01
+ else:
+ flow_control_value = 0x00
+
+ data_bits_value = None
+ if self._bytesize == 5:
+ data_bits_value = 0x00
+ elif self._bytesize == 6:
+ data_bits_value = 0x01
+ elif self._bytesize == 7:
+ data_bits_value = 0x02
+ elif self._bytesize == 8:
+ data_bits_value = 0x03
+ else:
+ raise ValueError('Invalid char len: {!r}'.format(self._bytesize))
+
+ stop_bits_value = None
+ if self._stopbits == serial.STOPBITS_ONE:
+ stop_bits_value = 0x00
+ elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
+ stop_bits_value = 0x01
+ elif self._stopbits == serial.STOPBITS_TWO:
+ stop_bits_value = 0x01
+ else:
+ raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits))
+
+ configuration_report = struct.pack(
+ '>BLBBBB',
+ _REPORT_GETSET_UART_CONFIG,
+ self._baudrate,
+ parity_value,
+ flow_control_value,
+ data_bits_value,
+ stop_bits_value)
+
+ self._hid_handle.send_feature_report(configuration_report)
+
+ self._hid_handle.send_feature_report(
+ bytes((_REPORT_GETSET_UART_ENABLE, _ENABLE_UART)))
+ self._update_break_state()
+
+ @property
+ def in_waiting(self):
+ return self._read_buffer.qsize()
+
+ def reset_input_buffer(self):
+ if not self.is_open:
+ raise PortNotOpenError()
+ self._hid_handle.send_feature_report(
+ bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_RX_FIFO)))
+ # empty read buffer
+ while self._read_buffer.qsize():
+ self._read_buffer.get(False)
+
+ def reset_output_buffer(self):
+ if not self.is_open:
+ raise PortNotOpenError()
+ self._hid_handle.send_feature_report(
+ bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_TX_FIFO)))
+
+ def _update_break_state(self):
+ if not self._hid_handle:
+ raise PortNotOpenError()
+
+ if self._break_state:
+ self._hid_handle.send_feature_report(
+ bytes((_REPORT_SET_TRANSMIT_LINE_BREAK, 0)))
+ else:
+ # Note that while AN434 states "There are no data bytes in
+ # the payload other than the Report ID", either hidapi or
+ # Linux does not seem to send the report otherwise.
+ self._hid_handle.send_feature_report(
+ bytes((_REPORT_SET_STOP_LINE_BREAK, 0)))
+
+ def read(self, size=1):
+ if not self.is_open:
+ raise PortNotOpenError()
+
+ data = bytearray()
+ try:
+ timeout = Timeout(self._timeout)
+ while len(data) < size:
+ if self._thread is None:
+ raise SerialException('connection failed (reader thread died)')
+ buf = self._read_buffer.get(True, timeout.time_left())
+ if buf is None:
+ return bytes(data)
+ data += buf
+ if timeout.expired():
+ break
+ except Queue.Empty: # -> timeout
+ pass
+ return bytes(data)
+
+ def write(self, data):
+ if not self.is_open:
+ raise PortNotOpenError()
+ data = to_bytes(data)
+ tx_len = len(data)
+ while tx_len > 0:
+ to_be_sent = min(tx_len, 0x3F)
+ report = to_bytes([to_be_sent]) + data[:to_be_sent]
+ self._hid_handle.write(report)
+
+ data = data[to_be_sent:]
+ tx_len = len(data)
+
+ def _hid_read_loop(self):
+ try:
+ while self.is_open:
+ data = self._hid_handle.read(64, timeout_ms=100)
+ if not data:
+ continue
+ data_len = data.pop(0)
+ assert data_len == len(data)
+ self._read_buffer.put(bytearray(data))
+ finally:
+ self._thread = None
diff --git a/mixly/tools/python/serial/urlhandler/protocol_hwgrep.py b/mixly/tools/python/serial/urlhandler/protocol_hwgrep.py
new file mode 100644
index 00000000..1a288c94
--- /dev/null
+++ b/mixly/tools/python/serial/urlhandler/protocol_hwgrep.py
@@ -0,0 +1,91 @@
+#! python
+#
+# This module implements a special URL handler that uses the port listing to
+# find ports by searching the string descriptions.
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2011-2015 Chris Liechti
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# URL format: hwgrep://&