Как я переделал свой мини-ПК и зачем мне это было нужно

в 4:38, , рубрики: искуственный интеллект, мини-пк, переделка, сделай сам

Здравствуйте, дорогие друзья!
Купил я тут себе компутер. Выбирал, как водится, долго и мучительно — хотел мини-ПК, потому что давно проникся этим форм-фактором: компактный, экономичный, да и по цене приятнее, чем здоровенные системники.

В момент выбора, конечно, руководствовался главным критерием — ценой.
Мой взгляд пал на чудо инженерной мысли шэньжэньского производства — Gmtec mini pc K-6. За свои деньги он предлагает вполне бодрые характеристики, особенно если руки не из коробки, а из плеч.

так было
так было

Брал я его без ОЗУ и ПЗУ, с мыслью: «Сам поставлю — сам виноват».
В итоге воткнул 64 ГБ оперативки и 1 ТБ SSD, и понеслось: начал пробовать локальные модели ИИ. Забегая вперёд скажу: оно работает!
Но если бы сейчас выбирал снова — взял бы обычный стационарник. Потому что заставить миник крутить нейронки — это пытаться родить что-то крупное такими средствами.

Как я переделал свой мини-ПК и зачем мне это было нужно - 2

Кулеры, паяльник и немного магии

После пары дней нагрузочного ада железо взвыло и запросило дополнительного охлаждения. Не буду углубляться в тему — половина Хабра уже резала и пилила свои мини-ПК, — просто скажу, что вдохновлялся вот этим постом:
🔗 4PDA — Gmtec K-6 (тема модификаций)

многострадальная башня

многострадальная башня
кажется что болты насквозь

кажется что болты насквозь

Коротко суть:
— Пилил, сверлил, подгонял.
— В итоге поставил башенный кулер на процессор.
— Второй кулер (охлаждение памяти) заменил на 92 мм зверюгу.

кулер ожлаждения памяти
кулер ожлаждения памяти

Плюс 5 В → вход повышайки → выход 12 В на кулер.

У миника — 5 В, у кулеров — 12 В и 4 пина.
Решение: DC-DC повышайка в «разрыв» питания.
Подключение:

  1. Плюс 5 В → вход повышайки → выход 12 В на кулер.

  2. GND общий.

  3. PWM (управление скоростью, подтяжка к плюсу).

  4. Tach (датчик Холла, считает обороты).

Всё заработало! Кулеры крутятся, миник дышит.

Новый корпус и графическая станция мечты

Следующий квест — корпус. Тут выручили добрые люди с 4PDA: автор выложил STL-модели, а я немного модифицировал и распечатал новый корпус. Теперь это не мини-ПК, а мини-монстр.

Кроме того, прикупил графическую станцию и переходник M.2 → OCuLink, ведь у K-6 два гнезда под память. В планах — докинуть видеокарту и пустить её в бой за ИИ-величие.

Корпус в итоге получился почти размером с “Алису” — большую колонку от Яндекса.
И тут в голову прилетела идея:

А что, если добавить немного графической магии?

Анимация огня на матрице WS2812B

В освободившемся месте я поставил 8×8-матрицу WS2812B, а управляет всем ESP8266.
Он считывает температуру с DS18B20, установленного на радиаторе. Чем выше температура — тем ярче горит анимация пламени.
Комп думает → греется → загорается → выглядит круто.
Логично? Логично!

Самое забавное — код анимации я написал при помощи тех самых локальных моделей ИИ, что крутятся на этом минике. Вдохновлялся легендарной “Лампой Гайвера”.

Как я переделал свой мини-ПК и зачем мне это было нужно - 6

Также имеется веб интерфейс, при первом включении контроллер разворачивает точку доступа, далее можно подклюситься к wifi и отрегулировать минимальные и максимальные значения температуры датчика для эффекта пламени

В результате я получил:

  • 🔧 Незабываемые впечатления (особенно от работы болгаркой в туалете, выпиливая радиатор нужного размера);

  • 🧠 Ценный опыт;

  • 🌡️ И главное — снижение температуры градусов на 15 (по ощущениям, конечно, но кулеры шуршат довольные).

Вывод простой:
Если вы опытный инженер, который знает себе цену — не повторяйте это дома.
А если вы мечтатель-фантазёр, то... ну, вы уже и так знаете, что делать. 😄

Хочу сказать спасибо сообществу, которое делится опытом, и всем, кто не боится «пилить» в прямом и переносном смысле.
А я пошёл допиливать подсветку и думать, как прикрутить туда ещё что-нибудь умное.

P.S. Ниже выкладываю код огня выполнен в среде ардуино (может кому пригодится)

#include <FastLED.h>
#include <OneWire.h>
#include <DallasTemperature.h>

// ================== НАСТРОЙКИ СВЕТОДИОДОВ ==================
#define DATA_PIN D3
#define NUM_COLS 8
#define NUM_ROWS 8
#define NUM_LEDS (NUM_COLS * NUM_ROWS)
#define MATRIX_ZIGZAG true

CRGB leds[NUM_LEDS];

// ================== НАСТРОЙКИ ЭФФЕКТА ОГНЯ ==================
#define SPARKLES 1        // вылетающие угольки вкл/выкл

uint8_t matrixValue[NUM_ROWS][NUM_COLS];
uint8_t *line = NULL;
uint8_t pcnt = 0;

// Маска значений для формы огня (растянуто до 8x8)
const uint8_t valueMask[NUM_ROWS][NUM_COLS] PROGMEM = {
  { 32 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 , 32 },
  { 64 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 , 64 },
  { 96 , 32 ,  0 ,  0 ,  0 ,  0 , 32 , 96 },
  {128 , 64 , 32 ,  0 ,  0 , 32 , 64 ,128 },
  {160 , 96 , 64 , 32 , 32 , 64 , 96 ,160 },
  {192 ,128 , 96 , 64 , 64 , 96 ,128 ,192 },
  {224 ,160 ,128 , 96 , 96 ,128 ,160 ,224 },
  {255 ,192 ,160 ,128 ,128 ,160 ,192 ,255 }
};

// Маска оттенков для огня (8x8)
const uint8_t hueMask[NUM_ROWS][NUM_COLS] PROGMEM = {
  { 1 , 11 , 19 , 25 , 25 , 19 , 11 ,  1 },
  { 1 ,  8 , 13 , 19 , 19 , 13 ,  8 ,  1 },
  { 1 ,  8 , 13 , 16 , 16 , 13 ,  8 ,  1 },
  { 1 ,  5 , 11 , 13 , 13 , 11 ,  5 ,  1 },
  { 0 ,  1 ,  5 ,  8 ,  8 ,  5 ,  1 ,  0 },
  { 0 ,  0 ,  1 ,  5 ,  5 ,  1 ,  0 ,  0 },
  { 0 ,  0 ,  0 ,  1 ,  1 ,  0 ,  0 ,  0 },
  { 0 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 }
};

// ================== DS18B20 ==================
#define ONE_WIRE_BUS D6
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

float currentTemp = 30.0;           // начальная температура
unsigned long lastTempMillis = 0;
unsigned long lastRequestMillis = 0;
const unsigned long TEMP_INTERVAL = 2000; // обновление раз в 2 секунды
bool tempRequested = false;

// ================== ФУНКЦИЯ ДЛЯ ЗИГЗАГ-МАТРИЦЫ С ПЕРЕВОРОТОМ ==================
uint16_t getPixelNumber(uint8_t x, uint8_t y) {
  if (x >= NUM_COLS || y >= NUM_ROWS) return -1;

  uint8_t y_flipped = NUM_ROWS - 1 - y; // переворот по вертикали

  if (MATRIX_ZIGZAG) {
    if (x % 2 == 0) {
      return x * NUM_ROWS + y_flipped;
    } else {
      return x * NUM_ROWS + (NUM_ROWS - 1 - y_flipped);
    }
  } else {
    return x * NUM_ROWS + y_flipped;
  }
}

// ================== ФУНКЦИИ ЭФФЕКТА ОГНЯ ==================
void fireInit() {
  FastLED.clear();
  if (line == NULL) {
    line = (uint8_t*)malloc(NUM_COLS);
  }
  generateLine();
  memset(matrixValue, 0, sizeof(matrixValue));
  pcnt = 0;
}

void fireRelease() {
  if (line != NULL) {
    free(line);
    line = NULL;
  }
}

// Случайная генерация следующей линии
void generateLine() {
  for (uint8_t x = 0; x < NUM_COLS; x++) {
    line[x] = random8(64, 255);
  }
}

// Сдвиг всех значений в матрице на одну строку вверх
void shiftFireUp() {
  for (uint8_t y = NUM_ROWS - 1; y > 0; y--) {
    for (uint8_t x = 0; x < NUM_COLS; x++) {
      matrixValue[y][x] = matrixValue[y - 1][x];
    }
  }

  for (uint8_t x = 0; x < NUM_COLS; x++) {
    matrixValue[0][x] = line[x];
  }
}

// Отрисовка кадра с интерполяцией
void drawFireFrame(uint8_t pcnt) {
  int nextv;
  uint8_t effectBrightness = 255; // максимальная яркость

  // коэффициент яркости в зависимости от температуры (25–35)
  float tempFactor = (currentTemp - 25.0) / 10.0;  // 0.0–1.0
  tempFactor = constrain(tempFactor, 0.0, 1.0);

  for (uint8_t y = NUM_ROWS - 1; y > 0; y--) {
    for (uint8_t x = 0; x < NUM_COLS; x++) {
      nextv = (((100 - pcnt) * matrixValue[y][x] + pcnt * matrixValue[y - 1][x]) / 100) - pgm_read_byte(&(valueMask[y][x]));
      
      uint8_t brightness = uint8_t(max(0, nextv) * tempFactor);

      CRGB color = CHSV(
                     20 + pgm_read_byte(&(hueMask[y][x])), // оттенок огня
                     255,
                     brightness
                   );
      CRGB color2 = color.nscale8_video(effectBrightness);

      int idx = getPixelNumber(x, y);
      if (idx >= 0) leds[idx] = color2;
    }
  }

  for (uint8_t x = 0; x < NUM_COLS; x++) {
    uint8_t brightness = uint8_t(((100 - pcnt) * matrixValue[0][x] + pcnt * line[x]) / 100 * tempFactor);
    
    CRGB color = CHSV(
                   20 + pgm_read_byte(&(hueMask[0][x])),
                   255,
                   brightness
                 );
    CRGB color2 = color.nscale8_video(effectBrightness);

    int idx = getPixelNumber(x, 0);
    if (idx >= 0) leds[idx] = color2;
  }
}

// Основная функция эффекта огня
void fireRoutine() {
  if (pcnt >= 90) {
    shiftFireUp();
    generateLine();
    pcnt = 0;
  }

  drawFireFrame(pcnt);
  pcnt += 30;
}

// ================== НЕБЛОКИРУЮЩИЙ ОПРОС ТЕМПЕРАТУРЫ ==================
void updateTemperatureNonBlocking() {
  unsigned long currentMillis = millis();
  
  if (!tempRequested) {
    if (currentMillis - lastTempMillis >= TEMP_INTERVAL) {
      oneWire.reset();
      oneWire.skip();
      oneWire.write(0x44, 1); 
      
      tempRequested = true;
      lastRequestMillis = currentMillis;
    }
  } else {
    if (currentMillis - lastRequestMillis >= 750) {
      uint8_t data[12];
      oneWire.reset();
      oneWire.skip(); 
      oneWire.write(0xBE); 
      
      for (uint8_t i = 0; i < 9; i++) {
        data[i] = oneWire.read();
      }
      
      int16_t raw = (data[1] << 8) | data[0];
      currentTemp = (float)raw / 16.0;
      currentTemp = constrain(currentTemp, 25.0, 35.0);
      
      tempRequested = false;
      lastTempMillis = currentMillis;
      Serial.print("Температура: ");
      Serial.println(currentTemp);
    }
  }
}
// ================== ЭФФЕКТ ЗАГРУЗКИ "ОГНЕННАЯ КОМЕТА" ==================
void loadingEffect(uint8_t rounds = 5, uint16_t speed = 40, uint8_t tail = 12) {
  const uint8_t pathLen = (NUM_COLS * 2 + (NUM_ROWS - 2) * 2);
  uint8_t pathX[pathLen];
  uint8_t pathY[pathLen];

  uint8_t idx = 0;

  // === путь по периметру против часовой ===
  for (uint8_t x = 0; x < NUM_COLS; x++) { // верхняя сторона (слева направо)
    pathX[idx] = x; pathY[idx] = 0; idx++;
  }
  for (uint8_t y = 1; y < NUM_ROWS; y++) { // правая сторона (сверху вниз)
    pathX[idx] = NUM_COLS - 1; pathY[idx] = y; idx++;
  }
  for (int8_t x = NUM_COLS - 2; x >= 0; x--) { // нижняя сторона (справа налево)
    pathX[idx] = x; pathY[idx] = NUM_ROWS - 1; idx++;
  }
  for (int8_t y = NUM_ROWS - 2; y > 0; y--) { // левая сторона (снизу вверх)
    pathX[idx] = 0; pathY[idx] = y; idx++;
  }

  // === анимация ===
  for (uint8_t r = 0; r < rounds; r++) {
    for (int i = pathLen - 1; i >= 0; i--) {  // против часовой
      FastLED.clear();

      // голова + хвост
      for (uint8_t t = 0; t < tail; t++) {
        int posIndex = (i + t) % pathLen;
        int ledIndex = getPixelNumber(pathX[posIndex], pathY[posIndex]);
        if (ledIndex >= 0) {
          // плавный градиент: белый → жёлтый → оранжевый → красный
          uint8_t fade = map(t, 0, tail - 1, 0, 255);
          CRGB color;
          if (t == 0) {
            color = CRGB::White; // голова белая
          } else if (fade < 85) {
            color = blend(CRGB::White, CRGB::Yellow, fade * 3); // белый → жёлтый
          } else if (fade < 170) {
            color = blend(CRGB::Yellow, CRGB::Orange, (fade - 85) * 3); // жёлтый → оранжевый
          } else {
            color = blend(CRGB::Orange, CRGB::Red, (fade - 170) * 3); // оранжевый → красный
          }
          leds[ledIndex] = color.nscale8(255 - (t * (200 / tail))); // постепенное затухание
        }
      }

      FastLED.show();
      delay(speed);
    }
  }

  FastLED.clear();
  FastLED.show();
}

// ================== НАСТРОЙКА И ОСНОВНОЙ ЦИКЛ ==================
void setup() {
  Serial.begin(115200);
  delay(1000);

  FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(255);

  sensors.begin(); 

  // Эффект загрузки перед стартом огня
 loadingEffect(5, 40, 12); // 5 кругов, скорость 40 мс, хвост 12 пикселей


  fireInit();
  Serial.println("Инициализация завершена. Температура опрашивается без блокировки!");
}


void loop() {
  updateTemperatureNonBlocking();
  
  fireRoutine();
  FastLED.show();
  
  delay(25);
}

Автор: andrushai

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js