重装机兵重构3-剧情篇

上一篇重装机兵重构2-地图篇的后续

Day16

  1. 认识一些像素人物的生成工具;
  2. 增加持久化的能力,基于json 的文件的 SaveSystem的实现

Day17

  1. 认识 Unity 的 Timeline
  2. 认识 Timeline 的 Playable Track, Signal Track

Day18

  1. 利用 ProcessFrame 实现 Timeline 中两个 playableAsset 的暂停,等待用户输入才继续播放

  2. 认识 Timeline 的 Animation Track,实现人物移动 + 动画播放
    效果图

  3. 修复 PlayableDirector.Pause 会导致人物会回到初始地点的bug:

    1
    2
    //director.Pause();
    director.playableGraph.GetRootPlayable(0).Pause();

Day19

  1. 处理TextMeshPro的中文乱码:
    从windows-> TextMeshPro -> Font Asset Creator 创建没有效果
    从*.ttf -> creat ->TextMeshPro ->Font Asset -> SPF 这样可以
  2. 简单过场黑屏动画 天亮了 使用 UI -> Panel + UI -> TextMeshPro 实现
  3. 封装打字机,过场动画 天亮了 也可以使用

Day20

  1. 解决Visual Studio 的中文在Unity 乱码的问题:
    在VS里面安装 扩展 -> 管理扩展, 搜索Force UTF-8(No BOM) 2022
  2. 增加逻辑, 开始游戏 -> 三选一角色 + 用户名称 -> 存档
  3. 增加逻辑, 记载游戏 -> 选择存档 -> 进入游戏

Day21

  1. 实现gameObject 在 UI Document 的展示(通过将GameObject 渲染到 RenderTexture)

  2. 实现Player的prefab, 根据职业选择渲染不同的人物

Day22

  1. 根据玩家选择的职业, 动态更新Timeline中animation track的人物的gameobject, 以及更新animation track 中的 animation clip

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    private void UpdateAnimationTrack(PlayableDirector director)
    {
    var playerObject = GameObject.FindWithTag("Player");
    Animator playerAnimator = playerObject.GetComponentInChildren<Animator>();
    Debug.Log("playerAnimator is " + playerAnimator);
    foreach (var track in director.playableAsset.outputs)
    {
    if (track.streamName == "PlayerPosition" || track.streamName == "PlayerAnimation")
    {
    if (track.streamName == "PlayerAnimation")
    {
    UpdateAnimationClip(track, playerAnimator);
    }
    director.SetGenericBinding(track.sourceObject, playerAnimator);
    }
    }
    }
    private void UpdateAnimationClip(PlayableBinding track, Animator animator)
    {
    if (track.sourceObject is AnimationTrack animationTrack)
    {
    // 获取该 Track 上的所有绑定(这里是 Animation Clip)
    foreach (var clip in animationTrack.GetClips())
    {
    // 替换现有的 Animation Clip
    foreach (var newClip in animator.runtimeAnimatorController.animationClips)
    {
    if (clip.displayName == newClip.name)
    {
    clip.asset = newClip; // 将新的 Animation Clip 赋值给 Track
    break;
    }
    }
    }
    }

    }

  2. 增加postTimeline 脚本, 实现剧情中人物的销毁和常规人物的位置重置

  3. 在camera上面增加audio source, *.m4a 需要转成 *.wav

    1
    ffmpeg -i input.m4a output.wav

Day23

  1. 打字机支持加速(左键跳过)
  2. 利用UI Docuemnt 实现tab
    tabs

Day24

  1. 实现menu 的隐藏出现,menu里面包含退出,实现游戏的开始->退出->开始的闭环:
    注意UIDocument 的隐藏不要使用SetActive(false), 这样会导致按钮的事件丢失

至此, 素材篇基本结束了,后面进入功能篇

重装机兵重构2-地图篇

上一篇重装机兵重构的后续

Day12

  1. 认识Aseprite, 一个Animated Sprite Editor & Pixel Art Tool。这个工具可以快速把真实的画转成像素风
  2. 认识Tiled, 比Unity自带的Palette 多了一个地形集,画地图更方便
  3. 认识SuperTiled2Unity 可以把Tiled 的地图放入unity里面
  4. 把多个Aseprite 合并成一个
    1
    2
    3
    4
    C:\Users\amanoooo\Desktop\Aseprite\aseprite --batch building-2.aseprite building3.aseprite building4.aseprite building5.aseprite building6.aseprite building7.aseprite building8.aseprite --sheet buildings.png


    C:\Users\amanoooo\Desktop\Aseprite\aseprite --batch cliff1.aseprite cliff2.aseprite cliff3.aseprite garbge.aseprite grass.aseprite grass2.aseprite hedge.aseprite hedge2.aseprite hedge-left.aseprite shizijia.aseprite stone.aseprite tree1.aseprite water1.aseprite sand.aseprite 三个点.aseprite 宝箱1.aseprite 地板1.aseprite 地板2.aseprite 地板3.aseprite 工作台.aseprite 平台.aseprite 油桶1.aseprite 油桶2.aseprite --sheet envs.png```

Day13

  1. 在Tiled 上面增加对象层,增加对象, 然后unity 使用脚本实现把对象改成IsTrigger ,实现门碰撞逻辑, 通过SuperCustomProperties 来获取门的名称, 实现不同门的跳转. 脚本如下

DoorManager 放在空对象上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using UnityEngine;
using SuperTiled2Unity;

public class DoorManager : MonoBehaviour
{
void Start()
{
// 获取所有的 SuperObject 组件

// 使用 FindObjectsByType 替代 FindObjectsOfType
SuperObject[] superObjects = FindObjectsByType<SuperObject>(FindObjectsSortMode.None);


foreach (SuperObject superObject in superObjects)
{
// 检查对象是否有碰撞器
Collider2D collider = superObject.GetComponent<Collider2D>();
if (collider != null)
{
// 动态添加 TriggerDoorNamePrinter 脚本
superObject.gameObject.AddComponent<DoorHandler>();
}
}
}
}

DoorHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using UnityEngine;
using SuperTiled2Unity;

[RequireComponent(typeof(Collider2D))] // 确保对象有 Collider2D 组件
public class DoorHandler : MonoBehaviour
{
private SuperCustomProperties customProperties;

void Start()
{
// 获取 SuperCustomProperties 组件
customProperties = GetComponent<SuperCustomProperties>();

// 确保碰撞器是触发器
Collider2D collider = GetComponent<Collider2D>();
if (collider != null)
{
collider.isTrigger = true;
}

// 打印 doorName 的值
PrintDoorName();
}

void OnTriggerEnter2D(Collider2D other)
{
// 当触发器被触发时打印 doorName
PrintDoorName();
}

private void PrintDoorName()
{
if (customProperties != null)
{
// 检查是否有 doorName 属性
if (customProperties.TryGetCustomProperty("doorName", out CustomProperty property))
{
Debug.Log($"Door Name: {property.m_Value}", this);
}
else
{
Debug.LogWarning("No doorName property found on this object.", this);
}
}
else
{
Debug.LogWarning("No SuperCustomProperties component found on this object.", this);
}
}
}

Day14

  1. 实现不同场景人物的初始化位置:
    在Awake 里面查找自定属性 IsEntry
  2. fix迭代更新tmx对应的tsx对应的png(补充素材),出现的问题是unity的地图素材乱了,和tiled的不一致,通过更新tsx的高度修正bug

Day15

  1. 认识Tiled editor 增加类
  2. 实现多场景多入口的切换:
    进入IsDoor 的时候, 检查DoorName(场景名) ,检查 DoorIndex, 然后切换到对应的 Scene,查找EntryIndex==DoorIndex的 IsEntry 的gameObject
  3. 临时处理人物位移 x+0.5, y-0.5

Day16

  1. 处理bug:
    老版本的MetalMax有个逻辑是,场景A->B通过楼梯, B->A也是通过楼梯, 要求是到B的初始化状态是在楼梯上, 这时候按照我们的写法, B会因为OnTriggerEnter2D 立即切换到A, 解决方案是在 IsEntry 上面也增加一个Trigger, 只有 OnTriggerExit2D 才能是IsDoor 的trigger启用
    代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    using UnityEngine;
    using SuperTiled2Unity;
    using static UnityEngine.EventSystems.EventTrigger;

    [RequireComponent(typeof(Collider2D))] // 确保对象有 Collider2D 组件
    public class DoorHandler : MonoBehaviour
    {
    private SuperCustomProperties customProperties;
    private bool disableByEntry = false;


    void Awake()
    {
    // 获取 SuperCustomProperties 组件
    customProperties = GetComponent<SuperCustomProperties>();

    var isDoor = IsDoor();
    if (IsDoor())
    {
    // 确保碰撞器是触发器
    Collider2D collider = GetComponent<Collider2D>();
    if (collider != null)
    {
    collider.isTrigger = true;
    }
    }

    // 检查是否是入口门
    if (IsEntry())
    {

    Collider2D collider = GetComponent<Collider2D>();
    if (!isDoor)
    {
    collider.enabled = false;
    }

    if (GetEntryIndex() == SceneLoader.Instance.DoorIndex)
    {
    if (isDoor)
    {
    disableByEntry = true;
    }
    MovePlayerToThisDoor();
    }
    }
    }

    private void Update()
    {
    }


    void OnTriggerEnter2D(Collider2D other)
    {
    if (!disableByEntry)
    {
    SceneLoader.Instance.SwitchScene($"mm/scenes/{GetDoorName()}");
    } else
    {
    Debug.Log("ignore scene loader due to disableByEntry: " + disableByEntry);
    }
    }
    void OnTriggerExit2D(Collider2D other)
    {
    disableByEntry = false;
    }

    private int GetDoorIndex()
    {
    if (customProperties.TryGetCustomProperty("DoorIndex", out CustomProperty DoorIndex))
    {
    if (DoorIndex != null)
    {
    return DoorIndex.m_Value.ToInt();
    }
    }
    return 0;
    }

    private int GetEntryIndex()
    {
    if (customProperties.TryGetCustomProperty("EntryIndex", out CustomProperty Index))
    {
    if (Index != null)
    {
    return Index.m_Value.ToInt();
    }
    }
    return 0;
    }

    private string GetDoorName()
    {
    if (customProperties != null)
    {
    // 检查是否有 doorName 属性
    if (customProperties.TryGetCustomProperty("DoorName", out CustomProperty DoorName))
    {
    var DoorIndex = GetDoorIndex();
    Debug.Log($"Door Name: {DoorName.m_Value} Index:{DoorIndex}", this);
    SceneLoader.Instance.DoorIndex = DoorIndex;
    return DoorName.m_Value;
    }
    else
    {
    Debug.LogWarning("No doorName property found on this object.", this);
    return null;
    }
    }
    else
    {
    Debug.LogWarning("No SuperCustomProperties component found on this object.", this);
    return null;
    }
    }

    private bool IsEntry()
    {
    if (customProperties != null)
    {
    // 检查是否有 IsEntry 属性
    if (customProperties.TryGetCustomProperty("IsEntry", out CustomProperty property))
    {
    return property.m_Value == "true";
    }
    }
    return false;
    }

    private bool IsDoor()
    {
    if (customProperties != null)
    {
    // 检查是否有 IsEntry 属性
    if (customProperties.TryGetCustomProperty("IsDoor", out CustomProperty property))
    {
    return property.m_Value == "true";
    }
    }
    return false;
    }

    private void MovePlayerToThisDoor()
    {
    // 查找玩家对象
    GameObject player = GameObject.FindGameObjectWithTag("Player");
    Debug.Log($"player is {player}");
    if (player != null)
    {
    Vector3 alignedPosition = new Vector3(
    transform.position.x + 0.5f,
    transform.position.y - 0.5f,
    transform.position.z);
    // 将玩家移动到门的位置
    player.transform.position = alignedPosition;
    }
    else
    {
    Debug.LogWarning("Player not found in the scene.", this);
    }
    }
    }

效果

至此, 素材篇基本结束了,后面进入剧情篇

unity对话框打印机特效

分享一下我的对话框打印机特效

上下文

uGUI 已经变成弃用了,我使用的最近 unity 6 的 UI Document。

小镇Town 场景有一个 TownUI 的 UIDocument, TownUI里面有一个类似 Prefab 的Dialog 的UIDocuement, Dialog 里面是一个VisualElement 作为背景, 里面是ScrollView, 然后里面是Label, 你会看到我的代码里也是这么查询的。

TownHandler 变成static 是方便在变得场景中随时调用

showStr 变成public 是为了支持可以在 inspector 那边随时更改

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
using UnityEngine;
using UnityEngine.UIElements;
using System.Collections;

public class TownHandler : MonoBehaviour
{

public static TownHandler instance { get; private set; }



public float displayTime = 4.0f;
private VisualElement m_NonPlayerDialogue;
private float m_TimerDisplay;

public string showStr; //需要打出来的字
private Label label; //打字展示的文本
private ScrollView scrollView;




// Awake is called when the script instance is being loaded (in this situation, when the game scene loads)
private void Awake()
{
instance = this;
}


// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{

UIDocument uiDocument = GetComponent<UIDocument>();
//m_Healthbar = uiDocument.rootVisualElement.Q<VisualElement>("HealthBar");
//SetHealthValue(1.0f);


m_NonPlayerDialogue = uiDocument.rootVisualElement.Q<VisualElement>("Dialog");
m_NonPlayerDialogue.style.display = DisplayStyle.None;
m_TimerDisplay = -1.0f;


scrollView = m_NonPlayerDialogue.Q<ScrollView>();
label = scrollView.Q<Label>(); // 使用 Label 的名称

Debug.LogWarning("label is " + label);
}

// Update is called once per frame
void Update()
{
//if (m_TimerDisplay > 0)
//{
// m_TimerDisplay -= Time.deltaTime;
// if (m_TimerDisplay < 0)
// {
// m_NonPlayerDialogue.style.display = DisplayStyle.None;
// }


//}
}

public void DisplayDialogue()
{
Debug.Log("DisplayDialogue");
m_NonPlayerDialogue.style.display = DisplayStyle.Flex;
m_TimerDisplay = displayTime;

StartCoroutine(PrintWord());

}

IEnumerator PrintWord()
{

foreach (char letter in showStr)
{
label.text += letter; // 添加一个字符
yield return new WaitForSeconds(0.1f); // 模拟打印速度,调整时间来控制速度


scrollView.scrollOffset = new Vector2(0, label.resolvedStyle.height); // 设置滚动位置到最底部
}



}

}

重装机兵重构

背景

看到 《大灾变:黑暗之日》 竟然还在维护,突然很感动,想到了重装机兵在国内应该也有一定的受众。于是,再次开始游戏吧。

过程

Day1

  1. 安装 Unity Hub
  2. 按照这个视频一步步学习简单的2d物理引擎 https://learn.unity.com/tutorial/playground-get-started-on-your-first-game?language=en#

Day2

按照这个教程开始2d游戏的高阶基础 https://learn.unity.com/tutorial/set-up-tilemap-collision?uv=2022.3&courseId=64774201edbc2a1638d25d18&projectId=6477424bedbc2a1473e5fce4# ,

人物可以移动,可以和sprite碰撞, 也可以和tilemap 碰撞

  1. 认识 Main window
  2. 认识 Context Menu
  3. 认识 Hierarchy Menu
  4. 认识 Project Menu
  5. 认识 Inspector Menu
  6. 认识 物理引擎 Rigidbody 2D
  7. 认识 碰撞器 Box Collider 2D
  8. 认识 Grid + TileMap
  9. 认识 Palette (tileSet)
  10. 认识 Rect Tool
  11. 认识 prefab 的作用

Day3

继续昨天的教程, 人物移动可以扣血回血, 并在血条上体现, enemy 的巡逻
  1. 认识 Box Collider 2D 的is Trigger 会导致可以穿透,并有事件
  2. 认识 Box Collider 2D 的 auto Tiling
  3. 认识 UI Builder(UI Document)
  4. 认识 VisualElement
  5. PlayerController 的碰撞脚本和静态的 UIHandler 进行血量交互
  6. 认识 Animator
  7. 认识 Animation
  8. 认识 Blend Tree

Day4

  1. Blend Tree 增加 transtion
  2. 认识 Layers (用于发射子弹)
  3. 认识 Edit > Project Settings… > Physics 2D -> the Layer Collision Matrix
  4. 认识 Raycasting
  5. 遇到了子弹射出去不移动的问题
  6. 遇到了子弹从enemy身上穿过去的问题
  7. 实现了对话弹出ui
  8. 认识cinimachine (摄像头碰撞)

Day5

  1. 认识 Audio Source
  2. 认识 Audio Source 的 Spatial Blend
  3. 认识 Effects -> Particle System
  4. 认识 粒子的 Radius 和 angle
  5. 如何手搓动画, 主要是关键帧
  6. Editor -> Project Setting -> Player 设置分辨率,版本
  7. File -> Building Profile -> 设置scene -> Build And Run

Day6

  1. 使用 UIDocument 增加游戏入口 Main , 监听 Enter 利用 SceneManager 切换到另一个场景
  2. unity store 购买免费的 DOTWEEN 插件, 然后实现场景切换时候的fade动画

Day7

  1. 复习实现 NPC 的弹窗对话
  2. 文字的打字机特效
  3. 自动滚动到ScrollView 的最下面

Day8

  1. 修复刚进入场景是 animator 迟缓(关闭Transitions 的 has Exit time)
  2. 在Moving(Blend Tree) 的基础上增加 Idle 的 Blend Tree
  3. 把6张真人的照片 去除背景, 然后处理成 像素图
  4. 学习战斗系统 英文教程, 在b站上看的盗版的
  5. 认识 Rigidbody 2D 的 Body Type: Dynamic / Kinematic / Static
  6. 认识 Rigidbody 2D 的 Collistion Detection: Discrete / Continuous

2025春节

Day9

  1. 参考b站上的中文教程
  2. 认识 Physics2D.OverlapCircle, 实现仇恨范围, 敌人自动追踪
  3. 认识 Cinemachine vs Camera Folllow

Day10

  1. 认识PlayerInput(Script) 和 InputAction 的区别
  2. 认识跨脚本调用,除了使用static 方法, 还可以使用 gameObject.BroadcastMessage()
  3. 在animation里面插入关键帧, 实现动画打击(假设是4帧, 就是1,2 帧 disable, 3帧enable, 4帧disable)

Day11

  1. 利用Collider 的 isTriger实现击退效果, 被伤害动画
  2. 实现死亡效果(死亡的动画插入event, 然后Destory掉 gameObject)
  3. 认识 TextMeshPro, 实现漂浮文字
  4. 实现漂浮文字的暴击效果, 动画效果

至此,动画相关除了持久化都已经测试结果,开始地图篇

docker 容器重启追踪

背景

无意中看到 docker ps 显示容器重启了, 虽然服务正常,还是准备查询一下

步骤

确认到服务重启

找到上一个停止的容器的日志

查看日志

发现容器是正常停止的, 基本排除服务器重启

确认机器没有重启

查看docker日志

可以发现是node 的状态从new 变成了down

查看系统日志

监控到软件oom 了

1
journalctl -e


可以在aliyun 的监控看到当时cpu有增加,但是只有75%

结论

prometheus 的容器oom导致的docker node 节点的down, 但还不知道为什么会导致我的traefik 的容器重启,暂时的解决方式是给 prometheus 加上资源限制

openwrt上面克隆git仓库

问题与背景

为了进一步升级openwrt的能力范畴,很多服务又没有docker镜像, 所以只能在wrt上面跑代码,这样就涉及到了git克隆仓库,但是openwrt默认的ssh client是dropbear 的实现方式,这是一个针对小内存设备的特别版本,使用 git clone git@github.com:amanoooo/amanosblog.git 就会报错

现象

1
2
3
4
5
6
7
root@amanoswrt:~/amano# git clone git@github.com:amanoooo/amanosblog.git
Cloning into 'amanosblog'...
Connection closed by 20.205.243.166 port 22
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

解决

  1. 第一步安装openssh-client

  2. 检查可以发现ssh已经链接到openssh 了

    1
    2
    3
    4
    root@amanospi:~# which ssh
    /usr/bin/ssh
    root@amanospi:~# ls -al /usr/bin/ssh
    lrwxr-xr-x 1 root root 24 Jan 6 16:57 /usr/bin/ssh -> /usr/libexec/ssh-openssh
  3. 更新 ~/.ssh/config 文件, 增加下面的配置

    1
    2
    3
    Host github.com
    Hostname ssh.github.com
    Port 443

tspl打印图片

背景

上一篇文章聊了 nodejs 打印标签, 现在需要增加难度,把图片打印上去

解决方案

参考这篇文章, 作者已经实现了, 但是他的写法已经不支持最新的版本了, 我来翻新一下

新代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
const usb = require('usb');
const { Jimp, intToRGBA } = require('jimp');
console.log('Jimp is ', Jimp);
const fs = require('fs')
let iconv = require('iconv-lite');

const json = JSON.parse(fs.readFileSync('logo.json', { encoding: 'utf-8' }))
const imgWidthInBytes = json[0].length;
const imgHeightInDots = json.length;


const cmds = [
'SIZE 48 mm, 10 mm',
'CLS',
'TEXT 1,1,"TSS24.BF2",0,1,1,"你好"',
'PRINT 1',
'END'
]
// const cmds = [
// 'SIZE 48 mm,25 mm',
// 'CLS',
// 'TEXT 10,10,"4",0,1,1,"HackerNoon"',
// 'TEXT 10,40,"4",0,1,1,"amano"',
// 'BARCODE 10,100,"128",90,1,0,2,2,"altospos.com"',
// 'PRINT 1',
// 'END',
// ];



async function getImageData(path) {
const img = await Jimp.read(path)
const bitmap = img.bitmap
const widthInBytes = Math.ceil(bitmap.width / 8);
const data = new Array(bitmap.height);
for (let y = 0; y < bitmap.height; y++) {
const row = new Array(widthInBytes);
for (let b = 0; b < widthInBytes; b++) {
let byte = 0;
let mask = 128;
for (let x = b * 8; x < (b + 1) * 8; x++) {
const color = intToRGBA(img.getPixelColor(x, y));
if (color.a < 65) byte = byte ^ mask;
mask = mask >> 1;
}
row[b] = byte;
}
data[y] = row;
}
return data;
}



function print(cmds) {
let device = usb.findByIds(1137, 85)

console.log('cmds is ', cmds);

device.open();
device.interfaces[0].claim();
const outEndpoint = device.interfaces[0].endpoints.find(e => e.direction === 'out');
outEndpoint.transferType = 2;

const processedCmds = cmds.map(cmd => {
if (cmd.startsWith('BITMAP')) {
return Buffer.from(cmd);
}
else if (cmd.startsWith('RAW ')) {
// 去掉 'HEX ' 前缀并将十六进制字符串转换为 Buffer
const jsonData = JSON.parse(cmd.slice(4).trim());
console.log('jsonData is ', jsonData);
return Buffer.from(jsonData.flat());
} else {
// 普通命令按原样处理
return iconv.encode(cmd + '\r\n', 'gbk');
}
});

outEndpoint.transfer(Buffer.concat(processedCmds), (err) => {
if (err) {
console.error('Transfer error:', err);
}
device.close();
})
}


const main = async () => {

const list = usb.getDeviceList()
list.forEach(device => {
console.log(`Device: ${device.deviceDescriptor.idVendor}:${device.deviceDescriptor.idProduct}`);
});



print(cmds)

};



async function generateImg() {

const res = await getImageData('logo-niumag.png')
console.log('res is ', res);
fs.writeFileSync('./logo.json', JSON.stringify(res))
}

// generateImg()
main()

老代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const usb = require('usb');
const Jimp = require('jimp');

function getImageData(path, cb) {
Jimp.read(path, (err, img) => {
const widthInBytes = Math.ceil(img.getWidth() / 8);
const data = new Array(img.getHeight());
for (let y = 0; y < img.getHeight(); y++) {
const row = new Array(widthInBytes);
for (let b = 0; b < widthInBytes; b++) {
let byte = 0;
let mask = 128;
for (let x = b*8; x < (b+1)*8; x++) {
const color = Jimp.intToRGBA(img.getPixelColor(x, y));
if (color.a < 65) byte = byte ^ mask; // empty dot (1)
mask = mask >> 1;
}
row[b] = byte;
}
data[y] = row;
}
cb(data);
});
}

function print(buffer) {
// you can get all available devices with usb.getDeviceList()
let device = usb.findByIds(/*vid*/8137, /*pid*/8214);
device.open();
device.interfaces[0].claim();
const outEndpoint = device.interfaces[0].endpoints.find(e => e.direction === 'out');
outEndpoint.transferType = 2;
outEndpoint.transfer(buffer, (err) => {
device.close();
});
}

getImageData('hn-logo.png', (data) => {
const widthInBytes = data[0].length;
const heightInDots = data.length;

const buffer = Buffer.concat([
Buffer.from('SIZE 48 mm,25 mm\r\n'),
Buffer.from('CLS\r\n'),
Buffer.from(`BITMAP 10,20,${widthInBytes},${heightInDots},0,`),
Buffer.from(data.flat()),
Buffer.from('BARCODE 10,100,"128",50,1,0,2,2,"altospos.com"\r\n'),
Buffer.from('PRINT 1\r\n'),
Buffer.from('END\r\n'),
]);

print(buffer);
});

参考图片

1.
hn-logo.png

2.
logo-niumag.png

2024windows_powershell_乱码

背景

  1. 最近一些桌面端的项目启动的输出怎么乱码
  2. git 的一些操作也出现乱码

问题测试

可以复制这些到一个文件里面, 然后直接在powershell 里面执行

1
2
3
@echo off
echo test chinese character view 测试中文字符显示
pause

解决方案

方案1

  1. windows上我通过 chcp 65001 && npm run start 可以解决

方案2

  1. 参考这个 文章, 第一步查看powershell 配置位置
1
$PROFILE
  1. 在该配置文件中添加如下配置:
1
$OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = New-Object System.Text.UTF8Encoding
  1. 验证
1
./test.bat

2024docker使用代理

背景

和上一篇文章类似,墙内世界自己推送了image之后, 服务器(我的环境是centos)上拉取也得使用代理。
但是最近很多伙伴反馈只简单的更新环境变量没有效果,通过文档上面更新docker daemon.json 也没有效果

解决

还是参考官方文档, 使用 systemd unit file

  1. Create a systemd drop-in directory for the docker service:
1
sudo mkdir -p /etc/systemd/system/docker.service.d
  1. Create a file named /etc/systemd/system/docker.service.d/http-proxy.conf that adds the HTTP_PROXY environment variable:
1
2
3
4
5
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7890"
Environment="HTTPS_PROXY=http://127.0.0.1:7890"
Environment="NO_PROXY=localhost,127.0.0.1,docker-registry.example.com,.corp"

  1. Flush changes and restart Docker
1
2
sudo systemctl daemon-reload
sudo systemctl restart docker
  1. Verify that the configuration has been loaded and matches the changes you made, for example:
1
2
3
sudo systemctl show --property=Environment docker

Environment=HTTP_PROXY=http://127.0.0.1:7890 HTTPS_PROXY=http://127.0.0.1:7890 NO_PROXY=localhost,127.0.0.1,docker-registry.example.com,.corp

注意

我偷懒只写了http_proxy, 结果发现没有效果, 请一定不能省略https_proxy

2024安装clash

背景

https://github.com/Dreamacro/clash 因为晒车牌被抓住了,很多脚本都失效了,这里提供一个解决方案

方式

  1. 下载
1
2
3
wget https://archlinux.org/packages/extra/x86_64/clash/download
mv download clash.tar
tar -xvf clash.tar
  1. 运行
1
2
3
clash ./usr/bin/clash
# INFO[0000] Can't find MMDB, start download
# FATA[0000] Initial configuration directory error: can't initial MMDB: can't download MMDB: Get "https://cdn.jsdelivr.net/gh/Dreamacro/maxmind-geoip@release/Country.mmdb": read tcp 172.24.35.37:39168->8.7.198.46:443: read: connection reset by peer
  1. [可选]补充

如果第一步 archlinux 下载不了, 可以手动下载安装包, 然后 scp 到服务器上
如果第二步 Country.mmdb 下载不了, 可以手动下载, 然后 scp 到服务器的 ~/.config/clash 文件夹

  1. 更新配置

默认的配置文件是 ~/.config/clash 这里, 参考我的

1
2
3
4
➜  clash pwd
/root/.config/clash
➜ clash ls
cache.db config.yaml config.yaml.bak Country.mmdb
  1. 类unix命令行使用
1
export https_proxy=http://127.0.0.1:7890;export http_proxy=http://127.0.0.1:7890;export all_proxy=socks5://127.0.0.1:7890