如何转播电视信号并通过网页观看

前几天给大家简单介绍了一下如何使用树莓派搭建一个RTMP直播服务,并可以通过vlc等工具观看。这几天,我又改进了一下整个系统,现在可以通过网页观看直播,并通过MQTT服务控制一个Arduino发送红外信号,控制机顶盒。

现在,整套系统有这么几个东西。

  • 机顶盒
    • 通过网络接受直播信号
  • 采集卡
    • 采集机顶盒信号,并推送给PC
  • PC
    • 接受来自采集卡的信号,并推送给RTMP直播服务
  • 树莓派
    • RTMP服务,接收并转发视频信号
    • MMQT服务,通过网络发送信息给Arduino
    • PHP服务,通过网页收看直播,发送MMQT信息
  • Ardunio
    • 接收MMQT的信息,并根据信息发送红外信号控制机顶盒

机顶盒,采集卡,PC方面的事情就不详细说了,今天主要说一下树莓派和Ardunio的东西。

树莓派方面,还是给大家写了一个script,大家修改后可以直接运行。我也加了一些comment在里面。注意替换<your ip>, <your port>, <your service>之类的位置,运行之前记得搜一下有没有“<your”开头的地方,还有没有没有替换的

#!/bin/bash

# Raspberry Pi 5 RTMP Streaming Server Setup Script
# Installs NGINX with RTMP, configures HLS, and sets up systemd service
# Installs PHP and MMQT service

set -e

echo "🚀 Updating system..."
apt update && apt upgrade -y

echo "📦 Installing dependencies..."
apt install -y build-essential libpcre3 libpcre3-dev libssl-dev zlib1g-dev git wget


echo "📦 Installing mosquitto and mosquitto-clients..."
apt install -y mosquitto mosquitto-clients

echo "🔁 Enabling and starting mosquitto service..."
systemctl enable mosquitto
systemctl start mosquitto

echo "⚙️  Configuring Mosquitto for WebSockets..."
MOSQ_CONF="/etc/mosquitto/mosquitto.conf"

# Backup original config
cp $MOSQ_CONF ${MOSQ_CONF}.bak

cat <<EOF > $MOSQ_CONF
# Place your local configuration in /etc/mosquitto/conf.d/
#
# A full description of the configuration file is at
# /usr/share/doc/mosquitto/examples/mosquitto.conf.example

pid_file /run/mosquitto/mosquitto.pid

persistence true
persistence_location /var/lib/mosquitto/

log_dest file /var/log/mosquitto/mosquitto.log

include_dir /etc/mosquitto/conf.d

# MQTT listener
listener 1883
protocol mqtt

# WebSocket listener
listener 9001
protocol websockets

allow_anonymous true

# Optional authentication (uncomment below to enable)
# allow_anonymous false
# password_file /etc/mosquitto/passwd
EOF

echo "🔁 Restarting Mosquitto..."
systemctl restart mosquitto

echo "📦 Installing PHP and FPM..."
apt install -y php php-fpm php-cli

echo "✅ Ensuring PHP-FPM is running..."
# Double check the version of php
systemctl enable php8.2-fpm
systemctl start php8.2-fpm

echo "🌐 Downloading NGINX and RTMP module..."
cd /opt
git clone https://github.com/arut/nginx-rtmp-module.git
wget http://nginx.org/download/nginx-1.27.5.tar.gz
tar -zxvf nginx-1.27.5.tar.gz
cd nginx-1.27.5

echo "⚙️ Building NGINX with RTMP module..."
./configure --with-http_ssl_module --add-module=../nginx-rtmp-module
make -j$(nproc)
make install

echo "📝 Writing NGINX config with RTMP + HLS..."
cat > /usr/local/nginx/conf/nginx.conf <<'EOF'
user www-data;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    sendfile off;
    tcp_nopush on;
    directio 512;

    server {
        listen <your port>;
        root html;
    index index.php index.html index.htm;
        location / {
            try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;  # Update based on your PHP version
        }

        location /hls {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;
            add_header Cache-Control no-cache;
            add_header 'Access-Control-Allow-Origin' '*';
        }

    }
}

rtmp {
    server {
        listen 1935;
        chunk_size 4096;

        application <your service> {
            live on;
            record off;
            hls on;
            hls_path /tmp/hls;
            hls_fragment 3s;
            hls_playlist_length 60s;
        }
    }
}
EOF

echo "📁 Creating HLS output directory..."
mkdir -p /tmp/hls

echo "🔧 Creating systemd service for NGINX RTMP..."
cat > /etc/systemd/system/nginx-rtmp.service <<EOF
[Unit]
Description=NGINX RTMP Streaming Server
After=network.target

[Service]
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s stop
PIDFile=/usr/local/nginx/logs/nginx.pid
Type=forking
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

echo "🔁 Enabling and starting NGINX service..."
systemctl daemon-reexec
systemctl daemon-reload
systemctl enable nginx-rtmp
systemctl start nginx-rtmp


echo "📄 Creating MQTT PHP script..."
cat > /usr/local/nginx/html/send.php <<'EOF'
<?php
// Simple security token (optional)
$secret = $_POST['key'] ?? '';
if ($secret !== '<your key2>') {
    http_response_code(403);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

$msg = $_POST['msg'] ?? '';

// Publish to Mosquitto
$cmd = escapeshellcmd("mosquitto_pub -h localhost -t 'tv/remote' -m '$msg'");
exec($cmd, $output, $retval);

echo json_encode([
    'status' => $retval === 0 ? 'sent' : 'error',
    'msg' => $msg
]);
?>
EOF

cat > /usr/local/nginx/html/index.php <<'EOF'
<?php
$key = $_GET['key'] ?? '';
if ($key !== '<your key1>') {
    http_response_code(403);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}
$channels = [
    '体育频道' => [
        ['msg' => 's00720', 'name' => '纬来体育'],
        ['msg' => 's00721', 'name' => '纬来体育 备用'],
        ['msg' => 's01420', 'name' => 'ELTA体育1'],
        ['msg' => 's01421', 'name' => 'ELTA体育1 备用'],
        ['msg' => 's01430', 'name' => 'ELTA体育2'],
        ['msg' => 's01431', 'name' => 'ELTA体育2 备用'],
        ['msg' => 's01440', 'name' => 'ELTA体育3'],
        ['msg' => 's01441', 'name' => 'ELTA体育3 备用1'],
        ['msg' => 's01442', 'name' => 'ELTA体育3 备用2'],
        ['msg' => 's01450', 'name' => 'ELTA体育4'],
        ['msg' => 's01451', 'name' => 'ELTA体育4 备用'],
    ],
    'Reboot' => [
        ['msg' => 'r', 'name' => 'Reboot TV Box'],
        ['msg' => 'a', 'name' => 'Reboot TV App']
    ]
];

$secretKey = '<your key2>';
$streamKey = '<your stream key>';
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TV Streaming Panel</title>
    <style>
        body {
            margin: 0;
            font-family: "Segoe UI", sans-serif;
            background: #1c1c1c;
            color: white;
            display: flex;
            flex-direction: row;
            height: 100vh;
        }

        #player-container {
            flex: 1;
            background: black;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        video {
            width: 100%;
            height: 100%;
            background: black;
        }

        #sidebar {
            width: 300px;
            background: #2b2b2b;
            border-left: 2px solid #444;
            overflow-y: auto;
            padding: 20px;
            box-shadow: -2px 0 10px rgba(0,0,0,0.5);
            transition: width 0.3s ease;
        }

        h2 {
            font-size: 20px;
            color: #00bfff;
            margin: 0 0 10px;
        }

        .category {
            margin-bottom: 10px;
        }

        .category-header {
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            padding: 8px;
            background-color: #333;
            border-radius: 4px;
            user-select: none;
        }

        .category-header .arrow {
            display: inline-block;
            width: 1.2em;
        }

        .channel-list {
            display: none;
            margin-top: 5px;
            margin-left: 10px;
        }

        .channel-btn {
            display: block;
            width: 100%;
            margin: 6px 0;
            padding: 10px;
            background: #00bfff;
            color: #fff;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            cursor: pointer;
            text-align: left;
        }

        .channel-btn:hover {
            background: #0099cc;
        }

        .status {
            margin-top: 20px;
            font-size: 14px;
            color: #ccc;
        }

        /* 📱 Responsive styles */
        @media screen and (max-width: 768px) {
            body {
                flex-direction: column;
            }

            #sidebar {
                width: 100%;
                border-left: none;
                border-top: 2px solid #444;
                padding: 10px;
            }

            h2 {
                font-size: 18px;
            }

            .channel-btn {
                font-size: 16px;
                padding: 12px;
            }

            .category-header {
                font-size: 18px;
                padding: 10px;
            }
        }
    </style>
</head>
<body>

<div id="player-container">
    <video id="video" controls autoplay muted></video>
</div>

<div id="sidebar">
    <h2>Channel List</h2>

    <?php foreach ($channels as $category => $items): ?>
        <div class="category">
            <div class="category-header" onclick="toggleCategory(this)">
                <span class="arrow">▶</span> <?php echo htmlspecialchars($category); ?>
            </div>
            <div class="channel-list">
                <?php foreach ($items as $channel): ?>
                    <button class="channel-btn" onclick="changeChannel('<?php echo $channel['msg']; ?>')">
                        <?php echo htmlspecialchars($channel['name']); ?>
                    </button>
                <?php endforeach; ?>
            </div>
        </div>
    <?php endforeach; ?>

    <div class="status" id="status">Status: Ready</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
    const streamKey = "<?php echo $streamKey; ?>";
    const secretKey = "<?php echo $secretKey; ?>";
    const video = document.getElementById('video');
    const statusBox = document.getElementById('status');
    const src = 'http://<your ip>:<your port>/hls/' + streamKey + '.m3u8';

    if (Hls.isSupported()) {
        const hls = new Hls();
        hls.loadSource(src);
        hls.attachMedia(video);
    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
        video.src = src;
    }

    function changeChannel(msg) {
        statusBox.textContent = "Sending msg: " + msg + "...";

        fetch('send.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
                msg: msg,
                key: secretKey
            })
        })
        .then(res => res.json())
        .then(data => {
            if (data.status === 'sent') {
                statusBox.textContent = "✅ Msg sent: " + msg;
            } else {
                statusBox.textContent = "❌ Failed: " + (data.error || 'Unknown error');
            }
        })
        .catch(err => {
            statusBox.textContent = "❌ Network error: " + err.message;
        });
    }

    function toggleCategory(header) {
        const list = header.nextElementSibling;
        const arrow = header.querySelector('.arrow');
        const open = list.style.display === 'block';

        list.style.display = open ? 'none' : 'block';
        arrow.textContent = open ? '▶' : '▼';
    }

    // Auto-expand the first category on load
    document.addEventListener("DOMContentLoaded", () => {
        const firstHeader = document.querySelector(".category-header");
        if (firstHeader) toggleCategory(firstHeader);
    });
</script>

</body>
</html>
EOF

echo ""
echo "✅ All done!"
echo ""
echo "🎥 Set OBS to stream to: rtmp://<your ip>/<your service>/<your stream key>"
echo "📺 Watch via VLC or player: rtmp://<your ip>/<your service>/<your stream key>"
echo "🌐 Or in browser (HLS): http://<your ip>:<your port>/hls/<your stream key>.m3u8"
echo "✅ Setup complete. Visit http://<your ip>:<your port>/index.php?key=<your key1> to test."

总体来讲,这个script会安装nginx,并且设置php,rtmp,mmqt服务。并通过一个简单的网页实现从网页上观看直播信号,并通过mmqt发送一些信息出去。不过网页的安全性不是很高,基本授权是通过get发送一个密码,并检查这个强密码和script里hard code的密码一样不一样。另外一个事就是,因为网页端使用的HLS服务,会有大概30秒的延迟。如果用VLC直接观看rtmp信号的话,延迟会小很多。

Ardunio方面,要做两件事,第一件事是通过IR receiver来学习遥控器的信号。

#include <IRremote.h>

void setup() {
  Serial.begin(9600);
  IrReceiver.begin(8, ENABLE_LED_FEEDBACK); // Receiver connected to pin 8
  Serial.println("Ready to receive IR signal...");
}

void loop() {
  if (IrReceiver.decode()) {
    // Print full raw dump for replay later
    IrReceiver.printIRResultShort(&Serial);
    IrReceiver.printIRSendUsage(&Serial);
    Serial.println();
    IrReceiver.resume(); // Ready to receive the next
  }
}

首先,我是把接收器安装在了Ardunio的8号接口,这时,就可以用这个接收器看遥控器发出来的信号是什么了。比如,当我按确认键,就会收到一下信息

Protocol=NEC Address=0xFF40 Command=0xD Raw-Data=0xF20DFF40 32 bits LSB first Gap=1940000us Duration=68650us
	Send with: IrSender.sendNEC(0xFF40, 0xD, <numberOfRepeats>);

这时我们就知道,可以发送什么样的信号了。当学习完所有的需要按的信号后,就可以进入下一步。简单来说,就是连接WIFI(我用的Arduino 4 withi wifi),然后连接MMQT的服务(搭设在树莓派上的)。通过MMQT接收到什么信息,就解析出来,并执行相应的操作。比如我这里,s开头的信息,表示要选台,他会输入台号,并执行一些其他的操作。如果收到r,表示重启机顶盒,他会模拟遥控器的几次操作。这边可能就不能直接抄了,需要你自己修改一下。另外,当发送什么按键的时候,Arduino的灯就会亮起一个相应的数字或者按键,方便我知道发送的啥信号。

#include "Arduino_LED_Matrix.h"
#include <IRremote.h>
#include <PubSubClient.h>
#include "WiFiS3.h"
#include "secrets.h" 

ArduinoLEDMatrix matrix;
char ssid[] = <your wifi ssid>;        // your network SSID (name)
char pass[] = <your wifi pwd>;    // your network password (use for WPA)
int status = WL_IDLE_STATUS;

// MQTT broker settings
const char* mqtt_server = "<your ip>"; // your Raspberry Pi IP address
const int mqtt_port = 1883;
const char* mqtt_topic = "tv/remote";

WiFiClient wifiClient;
PubSubClient client(wifiClient);

void setup() {
  Serial.begin(9600);      // initialize serial communication

  // check for the WiFi module:
  if (WiFi.status() == WL_NO_MODULE) {
    Serial.println("Communication with WiFi module failed!");
    // don't continue
    while (true);
  }

  String fv = WiFi.firmwareVersion();
  if (fv < WIFI_FIRMWARE_LATEST_VERSION) {
    Serial.println("Please upgrade the firmware");
  }

  // attempt to connect to WiFi network:
  while (status != WL_CONNECTED) {
    Serial.print("Attempting to connect to Network named: ");
    Serial.println(ssid);                   // print the network name (SSID);

    // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
    status = WiFi.begin(ssid, pass);
    // wait 10 seconds for connection:
    delay(10000);
  }
  printWifiStatus();                        // you're connected now, so print out the status
  
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);

  matrix.begin();
  IrSender.begin(7); // Transmitter LED connected to pin 7
}

void loop() {
  // Reconnect Wi-Fi if disconnected
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi disconnected. Attempting reconnection...");
    while (WiFi.status() != WL_CONNECTED) {
      status = WiFi.begin(ssid, pass);
      delay(10000); // Wait for connection
    }
    printWifiStatus(); // Print new status once connected
  }

  // Reconnect MQTT if needed
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
}

void callback(char* topic, byte* message, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("]: ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)message[i]);
  }
  Serial.println();

  execute(String((char*)message));
}

void reconnect() {
  unsigned long startAttemptTime = millis();
  
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    if (client.connect("arduinoClient")) {
      Serial.println("connected");
      client.subscribe(mqtt_topic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      delay(2000);

      // Timeout after 30 seconds
      if (millis() - startAttemptTime > 30000) {
        Serial.println("MQTT reconnect timeout");
        break;
      }
    }
  }
}

void execute(String message) {
  char c = message[0];
  int i;
  int j;
  int num;
  switch(c) {
    case 's': // channel selection
      for (i = 1; i < message.length(); i++) {
        c = message[i];
        if (i < 5) { // channel number, 4 digits
          sendSignal(c);
          delay(500);
        }
        if (i == 5) { // process the last digit
          sendSignal('o'); // press ok
          num = c - '0'; // convert char to int
          delay(3000);
          for (j = 0; j < num; j++) {
            sendSignal('d'); // press 'down' based on the last digit
            delay(500);
          }
          if (num > 0) { // if 'down' is pressed, press 'ok'
            sendSignal('o');
          } else {
            sendSignal('b'); // close menu
          }
        }
      }
      break;
    case 'r': // restart tv box
      sendSignal('p'); // open power menu
      delay(1000);
      sendSignal('d'); // press down
      delay(1000);
      sendSignal('d'); // select reboot
      delay(1000);
      sendSignal('o'); // reboot
      delay(45000);
      sendSignal('o');
      delay(2000);
      sendSignal('r');
      delay(1000);
      sendSignal('r');
      delay(1000);
      sendSignal('o'); // select tv app
      delay(1000);
      sendSignal('o'); // enter tv app
      delay(10000);
      sendSignal('b'); // enter tv
      break;
    case 'a': // restart tv app
      sendSignal('b'); // back
      delay(1000);
      sendSignal('u'); // select quit
      delay(1000);
      sendSignal('o'); // confirm quit
      delay(3000);
      sendSignal('o'); // select tv
      delay(1000);
      sendSignal('o'); // enter tv
      delay(10000);
      sendSignal('b'); // enter tv
      break;
  }
  
  delay(1000);
  clearScreen();
}

void sendSignal(char c) {
  switch(c) {
    case '0':
      show0();
      IrSender.sendNEC(0xFF40, 0x0, 1);
      break;
    case '1':
      show1();
      IrSender.sendNEC(0xFF40, 0x1, 1);
      break;
    case '2':
      show2();
      IrSender.sendNEC(0xFF40, 0x2, 1);
      break;
    case '3':
      show3();
      IrSender.sendNEC(0xFF40, 0x3, 1);
      break;
    case '4':
      show4();
      IrSender.sendNEC(0xFF40, 0x4, 1);
      break;
    case '5':
      show5();
      IrSender.sendNEC(0xFF40, 0x5, 1);
      break;
    case '6':
      show6();
      IrSender.sendNEC(0xFF40, 0x6, 1);
      break;
    case '7':
      show7();
      IrSender.sendNEC(0xFF40, 0x7, 1);
      break;
    case '8':
      show8();
      IrSender.sendNEC(0xFF40, 0x8, 1);
      break;
    case '9':
      show9();
      IrSender.sendNEC(0xFF40, 0x9, 1);
      break;
    case 'o':
      showOK();
      IrSender.sendNEC(0xFF40, 0xD, 1);
      break;
    case 'b':
      showBack();
      IrSender.sendNEC(0xFF40, 0x42, 1);
      break;
    case 'p':
      showPower();
      IrSender.sendNEC(0xFF40, 0x4D, 1);
      break;
    case 'u':
      showUp();
      IrSender.sendNEC(0xFF40, 0xB, 1);
      break;
    case 'd':
      showDown();
      IrSender.sendNEC(0xFF40, 0xE, 1);
      break;
    case 'l':
      showLeft();
      IrSender.sendNEC(0xFF40, 0x10, 1);
      break;
    case 'r':
      showRight();
      IrSender.sendNEC(0xFF40, 0x11, 1);
      break;
  }
}

void show0() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void show1() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void show2() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void show3() {
  byte frame[8][12] = {
    { 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0 },
    { 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void show4() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void show5() {
  byte frame[8][12] = {
    { 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 },
    { 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void show6() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void show7() {
  byte frame[8][12] = {
    { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void show8() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void show9() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void showOK() {
  byte frame[8][12] = {
    { 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0 },
    { 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0 },
    { 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0 },
    { 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0 },
    { 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0 },
    { 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0 },
    { 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0 },
    { 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void showBack() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void showPower() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0 },
    { 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void showUp() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void showDown() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void showLeft() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
    { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void showRight() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void clearScreen() {
  byte frame[8][12] = {
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
  };
  matrix.renderBitmap(frame, 8, 12);
}

void printWifiStatus() {
  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your board's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}

然后就没然后……不过下一步可能看看如何让网页的声音可以在后台播放。我现在用iOS和chrome,如果把直播画面放到画中画,就可以在后台继续听直播。但是如果不用画中画,当我把屏幕关闭的时候,声音就会中断了,体验挺不好的,反正……

如何一键使用树莓派搭建RTMP直播服务

10年前,我写过一篇文章,给大家介绍了一下如何用最低的成本转播PlayStation 4到国内的直播平台。简单来说,就是用PS4自带的直播功能,推流到twitch,并使用树莓派截取信号并重新推送到其他的直播平台。这样做的好处是,不需要购买昂贵的视频采集卡,只需要一个树莓派就可以了。

10年后,我又有了类似的需求,只不过这次,我想做的是自己搭建一个直播服务来输出视频信号,这样,无论我身处何地,都可以通过网络来观看自己的视频。

当然,和10年前不同的,这次我购买了昂贵的视频采集卡,以及新的树莓派。并且,当时我研究了很久如何配置服务器之类的,这次chatgpt帮我一波搞定,并且连脚本都给我写好了。稍微调整一下就能用。所以也在这里分享给大家。这次的思路是,用树莓派搭建一个RTMP服务,然后用我的台式机将视频采集卡采集的内容通过OBS推给树莓派,这样我就可以通过VLS甚至浏览器直接观看内容了。

#!/bin/bash

# Raspberry Pi 5 RTMP Streaming Server Setup Script
# Installs NGINX with RTMP, configures HLS, and sets up systemd service

set -e

echo "🚀 Updating system..."
apt update && apt upgrade -y

echo "📦 Installing dependencies..."
apt install -y build-essential libpcre3 libpcre3-dev libssl-dev zlib1g-dev git wget

echo "🌐 Downloading NGINX and RTMP module..."
cd /opt
git clone https://github.com/arut/nginx-rtmp-module.git
wget http://nginx.org/download/nginx-1.27.5.tar.gz
tar -zxvf nginx-1.27.5.tar.gz
cd nginx-1.27.5

echo "⚙️ Building NGINX with RTMP module..."
./configure --with-http_ssl_module --add-module=../nginx-rtmp-module
make -j$(nproc)
make install

echo "📝 Writing NGINX config with RTMP + HLS..."
cat > /usr/local/nginx/conf/nginx.conf <<EOF
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    sendfile off;
    tcp_nopush on;
    directio 512;

    server {
        listen 8080;

        location / {
            root html;
            index index.html index.htm;
        }

        location /hls {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;
            add_header Cache-Control no-cache;
            add_header 'Access-Control-Allow-Origin' '*';
        }
    }
}

rtmp {
    server {
        listen 1935;
        chunk_size 4096;

        application live {
            live on;
            record off;
            hls on;
            hls_path /tmp/hls;
            hls_fragment 3s;
            hls_playlist_length 60s;
        }
    }
}
EOF

echo "📁 Creating HLS output directory..."
mkdir -p /tmp/hls

echo "🔧 Creating systemd service for NGINX RTMP..."
cat > /etc/systemd/system/nginx-rtmp.service <<EOF
[Unit]
Description=NGINX RTMP Streaming Server
After=network.target

[Service]
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s stop
PIDFile=/usr/local/nginx/logs/nginx.pid
Type=forking
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

echo "🔁 Enabling and starting NGINX service..."
systemctl daemon-reexec
systemctl daemon-reload
systemctl enable nginx-rtmp
systemctl start nginx-rtmp

echo ""
echo "✅ All done!"
echo ""
echo "🎥 Set OBS to stream to: rtmp://<RPI_IP_ADDRESS>/live/<key>"
echo "📺 Watch via VLC or player: rtmp://<RPI_IP_ADDRESS>/live/<key>"
echo "🌐 Or in browser (HLS): http://<RPI_IP_ADDRESS>:8080/hls/<key>.m3u8"

记得用sudo run一下就好了……这个脚本会帮你自动下载nginx和rtmp插件,自动编译和安装,自动配置,自动设置自动启动……

欢迎讨论……

酒后乱语(音乐)

查了一下,博客已经一年多没有更新了,今晚吃着鸡架和烤鱿鱼,喝了点朗姆可乐,有点感触,所以回来写篇文章。可能明天早上我就已经不记得今天都写过啥了。但是,这是自己的博客,忘了又怎样呢?

在过去的一年,我的身上也发生了很多事情,比如买房,搬家,生娃,公司裁员。虽然暂时没有裁到自己头上,但是也有种劫后余生的感觉。总之,听我播客的朋友应该可以体会到,我去年是有多么风雨飘摇。

但是今天想聊的不是我去年的生活,而是通过今晚喝的朗姆可乐,回忆起的中学时的一段往事。自从上大学,我养成了一个毛病,那就是周五晚上必须要稍微来一杯,虽然这个习惯由于某些原因暂停了一段时间,但是,周五晚上还是必须要得吃点好的。于是,今晚也是同样借着周五晚上,吃了一波烤鸡架和烤鱿鱼。由于是周五晚上嘛,叫外卖的等待时间比较久,于是我就跑去店里买了瓶朗姆酒,正好家里有冰可乐,于是在家鼓捣起了朗姆可乐。

说起来,朗姆可乐,也叫自由古巴,可能是肉壳第一款有概念的鸡尾酒。虽然我自己喝过的第一款广义上的鸡尾酒是7-11里买的杰克丹尼可乐,但是朗姆可乐在我的心目中还是有些和其他鸡尾酒不一样的地位。

大概,那还是我上初中的时候吧,在我很早以前写的文章改变自己的三首歌中,我提到过我爸给我买过的一套5张CD的精选集,让我喜欢上了欧美老歌,后来初中时,那5张CD都被我听了无数遍了,就开始在家里找找看有没有什么其他的CD,结果还真的被我找到了好多。其中有一张,我已经不太记得专辑里的其他歌曲了,但是里面有一首Rum & Coca-Cola让我印象深刻。虽然后来才知道演唱的是The Andrews Sisters,但这已经是后话了。当时年纪还小,也不喝酒,所以对Rum是什么并没有什么概念。但是吧,我还是知道Coca-Cola也就是可口可乐是什么。所以对这首歌就会有些特别的印象,外加歌曲本身欢快的节奏也非常得我心,于是就搜了一下这首歌的歌名,才知道了朗姆可乐这款鸡尾酒。也就像是我说的,这是我第一款有概念的鸡尾酒。

说个小插曲,我司内部的某个工具名叫Daiquery,这工具的图表示一个马提尼杯,就是那种V字形的高脚杯。我之前一直不知道为什么要用这个图标。最近才知道,这是因为这个工具的名字和Daiquiri这款鸡尾酒很像。只不过Daiquiri好像用玛格丽特杯多一点。

回到正题,你们懂得,喝了点酒嘛,就想找点歌听,于是在Spotify和Youtube上,以Rum & Coca-Cola开始搜起来。然后我突然意识到,现在的人们,想找首歌是多么的容易,只要你知道这首歌叫什么,几乎就没有找不到的。总有一款音乐App或者视频App能找到你想要的歌。真的非常羡慕现在的人,想要找一些在本时代相对冷门的音乐,是多么的容易。回想起15,20年前,在网络还不是那么发达的时代。因为我喜欢的音乐品类比较另类,即使在今天,和同龄人也没有什么共同话题。我最期待我妈带我,或者和朋友一起去淘打口盘。每次都能找到一些我认识的,或者我不认识的歌手。买盘回家一遍一遍的听。但是呢,能买到的盘终究是少数。想要知道更多的歌手,或者歌曲,还得自己想办法从网上找。也是在那个时候,我知道了熊猫音乐网(Panda123),关于这件事,我在刚才提到的改变自己的三首歌纪念老熊猫这篇文章中都有提及,这里就不赘述了。就是这歌网站,让我接触到了更多喜爱爵士乐和轻音乐的朋友,也让我开始了解到了很多相关的知识。说起来,这也是我在自己的播客中开办老声常弹节目的灵感源泉。

除了自己淘打口盘,以及熊猫音乐网,另外一个对我很有帮助的工具,是高中时,泽少介绍给我的一个软件,名字叫做soulseek。令我惊讶的是,时至今日,这个工具还一直存在,甚至到2021年还在更新。在我高中那会儿,P2P下载盛行,什么BT,电驴什么的,全都有。只可惜,中文互联网还是很少有我喜欢的音乐资源的下载。于是这款soulseek就变成我的宝贝工具。这样是一款P2P软件,但是专注于音乐分享。于是我就用这款软件,下载了很多音乐,时至今日,我的硬盘里,还有很多当年下载的mp3。虽然,我已经很少听了。但是这确实是我自己的一段珍贵的回忆。时至今日,在我听到某一首歌的时候,我还能回忆起第一次听到,或者第一次记住,亦或者是我经常听这首歌的时候,眼前浮现的场景。也确实是一段非常有趣的经历吧。

如今,我已经是一个30多岁的大叔,俨然不是那个初中高中的小孩子了,但是我的音乐喜好却一直没有改变过,他们带给我的,除了此时此刻身心的愉悦,也能帮我助我回忆起以前的快乐时光。可能,对于音乐类型的偏好,我这辈子都不会有什么特别大的改变了。但是我觉得这样也挺好的,不是么?

如何升级大气层Atmosphere到1.1.0以及以上版本

最近搬家太忙了,Switch吃灰了几个月。虽然没有想玩什么新游戏,但是就是想给系统啥的升个级。看到大气层又升级了好几个版本呢,而且1.1.0有个大更新。虽然我不知道都更新了啥,但是根据描述来说,原来的的几个重要文件都被改名了,非常的莫名其妙……比如fusee-primary.bin改成了fusee.bin, fusee-secondary.bin改成了package3。所以,在从1.1.0之前的版本更新到1.1.0的版本以及以后的版本,就不是简单的复制粘贴这么简单了。所以在这里记录一下,以防以后忘了……

先说一下我目前的NS的状态,unpatched的NS,目前的版本是大气层0.19.4版,双系统,OFW也就是正版系统还是9.x,CFW也就是盗版系统是12.0.3. hekate是5.5.7。这个教程只在我自己的NS上尝试过,破解有风险,大家自己承担哈。

在这个教程里,我只告诉大家如何升级大气层,至于怎么升级系统,请看这里。方法没变,就是注意版本号就好了,直接选最新的就行了。

另外,我是用的注入器是TegraRcmGUI,这个东西的用处就是选择想要通过哪个文件来引导启动系统。之前写的教程里有提到过这个东西,可以自己看一下。

另外感谢下面的教程教会我如何升级

https://nh-server.github.io/switch-guide/extras/updating/

首先,我们开始下载一堆东西。

Atmosphère 1.2.0 prerelease

  • 这个是大气层的主文件,以及引导文件,1.2.0是个预发布版本,我个人比较喜欢用新版本,所以就下的最新的。如果我说,在之前的教程里,下载的第二个文件叫fusee-primary.bin,现在叫fusee.bin了。
  • https://github.com/Atmosphere-NX/Atmosphere/releases
    • atmosphere-1.2.0-prerelease-f7f83b474+hbl-2.4.1+hbmenu-3.4.1.zip
    • fusee.bin

Sigpatches (Supports HOS firmware 13.0.0 and AMS 1.2.0-prerelease)

hekate v5.6.3 & Nyx v1.0.8

hekate_ipl.ini

先来总结一下我们现在有哪些文件了

  1. atmosphere-1.2.0-prerelease-f7f83b474+hbl-2.4.1+hbmenu-3.4.1.zip
  2. fusee.bin
  3. fusee.zip
  4. hekate_ctcaer_5.6.3_Nyx_1.0.8.zip
  5. hekate_ipl.ini

后面提到某个文件的时候,我会用代号来指代。

首先,我们先看SD卡里都有什么。

就是这些文件,首先,我们先删除3个文件,第一个是这张图里的sept这个文件夹,第二个是hekate_ctcaer_5.5.7.bin

第三个是atmosphere文件夹里的fusee-secondary.bin。

然后将1,3,4都解压到电脑本地的同一个目录下。

得到这些文件。然后把5,也就是hekate_ipl.ini这个文件复制到上图中bootloader的文件夹里。之后把这个文件夹里的内容,复制到SD卡中并覆盖同名文件。

然后把SD卡放回到NS上,通过TegraRcmGUI,使用hekate_ctcaer_5.6.3.bin来启动系统到Hekate。

点击右上角的Options

确认右下角的Update Reboot 2 Payload是ON,如果不是,点一下让他变成ON。然后就可以Save Options,并且关机了。

之后,再通过TegraRcmGUI,使用fusee.bin来启动机器,就大功告成了。

可以看到,我的AMS也就是大气层系统的版本号已经是1.2.0了。(版本号中的最后一个字母是E,说明这是虚拟系统,如果是运行在真实系统里,最后一个字母是S)

大功告成!

如何给使用大气层Atmosphere的Switch系统升级

10月17日更新:

如果你打算升级Atmosphere到1.1.0以上的版本。本教程可能不适用。请结合这篇文章来看。

—- 分割线 —-

肉壳最开始破解自己的Switch的时候,使用的是SX OS。所以之前写过一篇文章,介绍如何给使用SX OS的Switch升级系统

但是,自从2020年12月开始,SX OS就没有再更新了,新的Switch系统也不能使用了,所以我把自己的Switch转换到了大气层Atmosphere中。如果大家有兴趣可以看看这个教程

那么问题来,我们如何给使用Atmosphere的Switch升级NS的系统呢?其实比使用SX OS还要简单。

首先要说的是,我在这篇文章里,只记录我自己的Switch的转换和升级方式。我的Switch是最初的没有patch的,换句话说,就是可以软破的那批机器。其次,我的SX OS是装在emunand里的,也就是虚拟系统。而且是装在SD卡里,没有隐藏分区。另外,我的正版系统虽然没有在用了,但是一直保持者9.x的系统版本。如果你的机器和我不一样,请参考上面两个链接,虽然是英文的,但是讲解的非常清楚。最终,破解有风险,如果在破解在转换过程中出现任何问题,请大家自行承担风险。

和之前一样,大家要先去下载一些文件。

Firmware 12.0.3

Atmosphère 0.19.4

Sigpatches (Supports HOS firmware 12.0.3 and AMS 0.19.4)

hekate v5.5.7 & Nyx v1.0.4

OK,我们先来整理一下,我们现在应该有如下几个文件。后面在引用到下列文件时,直接用数字代替了。

  1. Firmware 12.0.3.zip
  2. atmosphere-0.19.4-master-2ab01ad3+hbl-2.4.1+hbmenu-3.4.0.zip
  3. fusee.zip
  4. hekate_ctcaer_5.5.7_Nyx_1.0.4.zip

首先,将1解压到随便一个新的文件夹中,我们就叫它Firmware 12.0.3。将这个文件夹拷贝到Switch的SD卡的根目录下。

如果你的大气层已经是最新版本的话,就啥也不用做了,如果不是的话,需要把2,3,4都解压到SD的根目录。

然后把SD卡放回机器,开机进入大气层系统。

点击相册,会看到下面的界面

请无视第一个程序,我忘记删了,哈哈。在大气层下,升级系统我们需要用到第二个程序Daybreak,点击进入。

点击Install

选择最上面的Firmware 12.0.3

稍等一下,直到Continue变亮,然后点击Continue。

点击Preserve settings

点击Install (FAT 32 + exFAT)。这里假设你的SD卡是用exFAT的格式。如果你的SD卡不能放比4G大的文件,那么你的SD卡就是FAT32格式。

继续点击Continue

坐等安装结束,点击Reboot重启机器。

进入系统后检查主机系统,已经是12.0.3了。

大概就是这样……