?>/script>'; } ?> Base práctica de desarrollo Widgets Magazine

Autor Tema: Base práctica de desarrollo  (Leído 32715 veces)

0 Usuarios y 1 Visitante están viendo este tema.

Desconectado jar229

  • Moderador
  • *
  • Mensajes: 4609
Re:Base práctica de desarrollo
« Respuesta #60 en: 19-05-2025, 09:04 (Lunes) »
Muy bueno sí señor  >:( >:( >:(

Desconectado Darktakan

  • **
  • Mensajes: 3
Re:Base práctica de desarrollo
« Respuesta #61 en: 19-05-2025, 19:03 (Lunes) »
Put4 máquina!  Revives un tema antiguo para sacarle brillo, como tantos otros >:(

Desconectado raphik

  • *****
  • Mensajes: 108
Re:Base práctica de desarrollo
« Respuesta #62 en: 27-05-2025, 18:35 (Martes) »
Put4 máquina!  Revives un tema antiguo para sacarle brillo, como tantos otros >:(
No me había dado cuenta de que este hilo tiene más de 10 años. ¡Cómo pasa el tiempo! En fin, este es uno de esos temas que quedan en el aire y, si no lo termino, reviento.

Desconectado raphik

  • *****
  • Mensajes: 108
Re:Base práctica de desarrollo
« Respuesta #63 en: 27-05-2025, 19:18 (Martes) »
Vamos con la creación de paquetes. Para explicarlo mejor, me apoyaré en un "solucionador de sudokus" que funciona de maravilla, pero que nunca echarás en falta en tu router.
Esta versión, además de resolver los sudokus, permite guardarlos y restaurarlos en el sistema de archivos UCI.

El código para resolver sudokus no es mío para nada. Está basado en el algoritmo de Peter Norvig (https://norvig.com/sudoku.html) y la adaptación a Lua de flyinghyrax (https://github.com/flyinghyrax/rm-sudoku-solver). Lo que no teníamos es la adaptación a LuCI/OpenWrt. En eso sí que tengo algo de culpa.

La primera vez que construí el paquete, instalé el SDK de OpenWrt. Pero no hace falta. El buildroot tampoco. Como la aplicación está escrita en Lua, no hay nada que compilar. Por otro lado, un paquete ipk es, esencialmente, un conjunto de archivos comprimidos. Y para eso ya tenemos el compresor tar de Linux.

Lua es independiente del hardware y corre en cualquier router OpenWrt sin importar su arquitectura. La aplicación la componen cuatro ficheros que se instalan en determinadas carpetas del router. Para su empaquetado se usan dos ficheros más.

Vamos a ello:

El el PC prepara esta estructura de directorios:

luci-app-sudoku/
├── usr/
│   └── lib/
│       └── lua/
│           └── luci/
│               ├── controller/
│               │   └── sudoku.lua
│               ├── model/
│               │   └── sudoku.lua
│               └── view/
│                   └── sudoku/
│                       └── sudoku.htm
├── etc/
│   └── config/
│       └── sudoku

└── control

En lugar de poner enlaces de descarga, que desaparecen cuando menos te lo esperas, pongo directamente el código:

Fichero usr/lib/lua/luci/contoller/sudoku.lua
Código: [Seleccionar]
module("luci.controller.sudoku", package.seeall)

function index()
    entry({"admin", "services", "sudoku"}, call("action_sudoku"), _("Sudoku Solver"), 90)
end

function action_sudoku()
    local model = require "luci.model.sudoku"
    local http = luci.http
    local form = http.formvalue
    local mensaje = ""
    local input_str = string.rep("0", 81)
    local result_str = nil

    local saved_boards = model.get_all_boards()

    local accion = form("accion")
    local input_board = model.get_board_from_form()

    -- Caso: sin acción (inicio o reset)
    if not accion then
        input_str = string.rep("0", 81)
        result_str = nil

    -- Caso: resolver sudoku
    elseif accion == "resolver" then
        input_str = model.board_to_string(input_board)

        -- Medir el tiempo de resolución
        local start_time = os.clock()
        local solved, solved_board = model.solve(input_board)
        local end_time = os.clock()
        local tiempo_resolucion = end_time - start_time

        if solved then
            result_str = model.board_to_string(solved_board)
            mensaje = string.format('<span style="color:var(--success-color-low);">¡Sudoku resuelto en %.2f segundos!</span>', tiempo_resolucion)
        else
            result_str = nil
            mensaje = '<span style="color:var(--error-color-low);">No se pudo resolver el sudoku.</span>'
        end

    -- Caso: guardar sudoku
    elseif accion == "guardar" then
        input_str = model.board_to_string(input_board)
        local nombre = form("nombre") or "sin_nombre"
        model.save_board(nombre, input_str)
        mensaje = '<span style="color:var(--success-color-low);">Sudoku guardado.</span>'
        saved_boards = model.get_all_boards()

    -- Caso: restaurar sudoku
    elseif accion == "restaurar" then
        local section = form("saved_section")
        local board_str = model.get_board(section)
        if board_str then
            input_str = board_str
            mensaje = '<span style="color:var(--primary-color-low);">Sudoku restaurado.</span>'
        else
            input_str = string.rep("0", 81)
            mensaje = '<span style="color:var(--error-color-low);">No se encontró el sudoku.</span>'
        end
        result_str = nil

    -- Caso: borrar sudoku
    elseif accion == "borrar" then
        local section = form("saved_section")
        model.delete_board(section)
        input_str = string.rep("0", 81)
        mensaje = '<span style="color:var(--error-color-low);">Sudoku borrado.</span>'
        result_str = nil
        saved_boards = model.get_all_boards()
    end

    luci.template.render("sudoku/sudoku", {
        input = input_str,
        result = result_str,
        mensaje = mensaje,
        saved_boards = saved_boards
    })
end

Fichero usr/lib/lua/luci/model/sudoku.lua
Código: [Seleccionar]
--[[
    modelo.lua - Módulo de gestión y resolución de Sudokus para LuCI/OpenWrt
    Copyright (C) 2025 raphik

    Basado en el algoritmo de Peter Norvig (https://norvig.com/sudoku.html)
    y en la adaptación a Lua de flyinghyrax (https://github.com/flyinghyrax/rm-sudoku-solver).

    Este programa es software libre: puedes redistribuirlo y/o modificarlo bajo los términos
    de la Licencia Pública General de GNU publicada por la Free Software Foundation, ya sea
    la versión 3 de la Licencia, o (a tu elección) cualquier versión posterior.

    Este programa se distribuye con la esperanza de que sea útil, pero SIN GARANTÍA ALGUNA;
    ni siquiera la garantía implícita MERCANTIL o de APTITUD PARA UN PROPÓSITO PARTICULAR.
    Consulta la Licencia Pública General de GNU para más detalles.

    Deberías haber recibido una copia de la Licencia Pública General de GNU junto a este programa.
    En caso contrario, consulta <http://www.gnu.org/licenses/>.

    Créditos:
      - Algoritmo original: Peter Norvig
      - Adaptación Lua: flyinghyrax
      - Integración y mejoras para OpenWrt/LuCI: raphik
--]]

--[[
    Este módulo implementa el modelo de datos para una aplicación de Sudoku en OpenWrt,
    utilizando el sistema de configuración UCI para almacenar, cargar y borrar tableros.
    Además, integra un solucionador avanzado basado en el algoritmo de Peter Norvig,
    adaptado en Lua a partir de la implementación de flyinghyrax.

    Funcionalidades principales:
      - Gestión de tableros: guardar, cargar, listar y eliminar sudokus en UCI.
      - Conversión entre formatos de tablero (cadena y array).
      - Resolución eficiente de sudokus, incluyendo puzzles de dificultad "diabólica".
      - Interfaz compatible con controladores y vistas existentes en LuCI.

    Fecha: 24/05/2025

    Ejemplo de uso:
      local modelo = require "luci.model.sudoku"
      local ok, solucion = modelo.solve(tablero)
--]]

local M = {}

local uci = require "luci.model.uci".cursor()
local sectiontype = "board" -- El tipo de sección es 'board'

-- === Gestión de tableros en UCI ===

function M.get_all_boards()
    local boards = {}
    uci:foreach("sudoku", sectiontype, function(s)
        boards[#boards+1] = {
            section = s['.name'],
            name = s.name or s['.name'],
            input = s.input or string.rep("0", 81),
            difficulty = s.difficulty or "?"
        }
    end)
    return boards
end

function M.get_board(section)
    return uci:get("sudoku", section, "input")
end

function M.save_board(name, input_str)
    local s = uci:add("sudoku", sectiontype)
    uci:set("sudoku", s, "name", name)
    uci:set("sudoku", s, "input", input_str)
    uci:commit("sudoku")
end

function M.delete_board(section)
    uci:delete("sudoku", section)
    uci:commit("sudoku")
end

function M.string_to_board(str)
    local board = {}
    for i = 1, #str do
        board[i] = tonumber(str:sub(i, i)) or 0
    end
    return board
end

function M.board_to_string(board)
    local t = {}
    for i = 1, 81 do
        t[#t+1] = tostring(board[i] or 0)
    end
    return table.concat(t)
end

function M.get_board_from_form()
    local board = {}
    for i = 0, 8 do
        for j = 0, 8 do
            local val = luci.http.formvalue("cell_"..i.."_"..j)
            board[i*9 + j + 1] = tonumber(val) or 0
        end
    end
    return board
end

-- === Solver Norvig (flyinghyrax adaptado) ===

local digits = "123456789"
local rows = {"A","B","C","D","E","F","G","H","I"}
local cols = {"1","2","3","4","5","6","7","8","9"}

local function cross(A, B)
    local res = {}
    for _, a in ipairs(A) do
        for _, b in ipairs(B) do
            res[#res+1] = a..b
        end
    end
    return res
end

local squares = cross(rows, cols)

local unitlist = {}
for _, r in ipairs(rows) do unitlist[#unitlist+1] = cross({r}, cols) end
for _, c in ipairs(cols) do unitlist[#unitlist+1] = cross(rows, {c}) end
for _, rs in ipairs{{"A","B","C"},{"D","E","F"},{"G","H","I"}} do
    for _, cs in ipairs{{"1","2","3"},{"4","5","6"},{"7","8","9"}} do
        unitlist[#unitlist+1] = cross(rs, cs)
    end
end

local units = {}
local peers = {}
for _, s in ipairs(squares) do
    units[s] = {}
    for _, u in ipairs(unitlist) do
        for _, sq in ipairs(u) do
            if sq == s then
                units[s][#units[s]+1] = u
                break
            end
        end
    end
    local ps = {}
    for _, u in ipairs(units[s]) do
        for _, sq in ipairs(u) do
            if sq ~= s then ps[sq] = true end
        end
    end
    peers[s] = {}
    for sq in pairs(ps) do table.insert(peers[s], sq) end
end

local function parse_grid(grid)
    local values = {}
    for _, s in ipairs(squares) do values[s] = digits end
    local i = 1
    for _, s in ipairs(squares) do
        local d = grid:sub(i,i)
        if digits:find(d, 1, true) then
            if not M._assign(values, s, d) then return false end
        end
        i = i + 1
    end
    return values
end

function M._assign(values, s, d)
    local other = values[s]:gsub(d, "")
    for i = 1, #other do
        if not M._eliminate(values, s, other:sub(i,i)) then return false end
    end
    return values
end

function M._eliminate(values, s, d)
    if not values[s]:find(d, 1, true) then return values end
    values[s] = values[s]:gsub(d, "")
    if #values[s] == 0 then return false end
    if #values[s] == 1 then
        local d2 = values[s]
        for _, s2 in ipairs(peers[s]) do
            if not M._eliminate(values, s2, d2) then return false end
        end
    end
    for _, u in ipairs(units[s]) do
        local dplaces = {}
        for _, s2 in ipairs(u) do
            if values[s2]:find(d, 1, true) then dplaces[#dplaces+1] = s2 end
        end
        if #dplaces == 0 then return false end
        if #dplaces == 1 then
            if not M._assign(values, dplaces[1], d) then return false end
        end
    end
    return values
end

local function search(values)
    if not values then return false end
    local solved = true
    for _, s in ipairs(squares) do
        if #values[s] ~= 1 then solved = false break end
    end
    if solved then return values end

    local min, smin = 10, nil
    for _, s in ipairs(squares) do
        if #values[s] > 1 and #values[s] < min then
            min, smin = #values[s], s
        end
    end
    for i = 1, #values[smin] do
        local d = values[smin]:sub(i,i)
        local vals = {}
        for k,v in pairs(values) do vals[k]=v end
        local attempt = search(M._assign(vals, smin, d))
        if attempt then return attempt end
    end
    return false
end

-- Interfaz compatible: resuelve un tablero (array de 81 números) y devuelve ok, solución
function M.solve(board)
    -- Convierte el array a cadena
    local t = {}
    for i = 1, 81 do t[#t+1] = tostring(board[i] or 0) end
    local str = table.concat(t)
    local values = parse_grid(str)
    if not values then return false, board end
    local result = search(values)
    if not result then return false, board end
    -- Devuelve la solución como array
    local solved = {}
    for _, s in ipairs(squares) do
        solved[#solved+1] = tonumber(result[s])
    end
    return true, solved
end

return M

Fichero usr/lib/lua/luci/view/sudoku/sudoku.htm
Código: [Seleccionar]
<%+header%>
<h2>Sudoku Solver</h2>
<div id="sudoku-mensajes" style="min-height:24px;"><%= mensaje or "&nbsp;" %></div>

<form id="sudoku-form" method="post" action="<%=REQUEST_URI%>">

<table class="table" id="sudoku-table" style="width:auto;">
<% for i=0,8 do %>
  <tr>
  <% for j=0,8 do
      local inicial = ""
      if input and #input == 81 then
          inicial = input:sub(i*9+j+1, i*9+j+1)
      end
      local valor = ""
      local clase = "cbi-input-text sudoku-inicial"
      local readonly = ""
      if result and #result == 81 then
          valor = result:sub(i*9+j+1, i*9+j+1)
          if inicial ~= "0" and inicial ~= "" then
              clase = clase .. " sudoku-inicial"
              readonly = "readonly"
          else
              clase = clase .. " sudoku-resuelto"
          end
      else
          valor = inicial
          if inicial ~= "0" and inicial ~= "" then
              clase = clase .. " sudoku-inicial"
          end
      end
      if valor == "0" then valor = "" end
  %>
    <td class="sudoku-cell sudoku-cell-<%=i%>-<%=j%>">
      <input type="text" name="cell_<%=i%>_<%=j%>" maxlength="1" value="<%=valor%>" class="<%=clase%>" <%= readonly %> style="text-align:center; width:2.5em;" />
    </td>
  <% end %>
  </tr>
<% end %>
</table>
<br>
<button type="submit" name="accion" value="resolver" class="cbi-button cbi-input-apply" id="btn-resolver">Resolver</button>
<input type="text" name="nombre" placeholder="Nombre para guardar" class="cbi-input-text" style="margin-left:1em;">
<button type="submit" name="accion" value="guardar" class="cbi-button" id="btn-guardar">Guardar</button>
<button type="button" class="cbi-button" id="btn-limpiar" style="margin-left:1em;">Limpiar tablero</button>
</form>

<form id="sudoku-acciones" method="post" action="<%=REQUEST_URI%>" style="margin-top:2em;">
  <label for="saved_section"><b>Sudokus guardados:</b></label>
  <select name="saved_section" id="saved_section" class="cbi-input-select">
    <% for i, s in ipairs(saved_boards or {}) do %>
      <option value="<%=s.section%>"><%=s.name%> [<%=s.difficulty%>]</option>
    <% end %>
  </select>
  <button type="submit" name="accion" value="restaurar" class="cbi-button" id="btn-restaurar">Restaurar</button>
  <button type="submit" name="accion" value="borrar" class="cbi-button cbi-input-remove" id="btn-borrar">Borrar</button>
</form>

<style>
#sudoku-table {
  border: 3px solid var(--text-color-high);
  border-collapse: collapse;
  background-color: var(--background-color-medium);
}

#sudoku-table td {
  border: 1px solid var(--text-color-medium);
  padding: 0;
}

#sudoku-table tr:nth-child(3n) td {
  border-bottom: 2.5px solid var(--text-color-medium);
}
#sudoku-table td:nth-child(3n) {
  border-right: 2.5px solid var(--text-color-medium);
}
#sudoku-table tr:first-child td {
  border-top: 2.5px solid var(--text-color-medium);
}
#sudoku-table td:first-child {
  border-left: 2.5px solid var(--text-color-medium);
}

.sudoku-inicial {
  font-weight: bold;
  color: var(--primary-color-low) !important;
  background: transparent !important;
}
.sudoku-resuelto {
  font-weight: normal;
  color: var(--error-color-low) !important;
  background: transparent !important;
}
#sudoku-table input[type="text"] {
  background: transparent !important;
  border: none;
  box-shadow: none;
}
</style>

<script>
document.addEventListener("DOMContentLoaded", function() {
  var msgDiv = document.getElementById("sudoku-mensajes");

  var btnResolver = document.getElementById("btn-resolver");
  if (btnResolver) {
    btnResolver.addEventListener("click", function() {
      if (msgDiv) msgDiv.innerHTML = '<span class="spinning" style="color:blue;">Resolviendo...</span>';
    });
  }

  var btnGuardar = document.getElementById("btn-guardar");
  if (btnGuardar) {
    btnGuardar.addEventListener("click", function() {
      if (msgDiv) msgDiv.innerHTML = '<span class="spinning" style="color:blue;">Guardando...</span>';
    });
  }

  var btnRestaurar = document.getElementById("btn-restaurar");
  if (btnRestaurar) {
    btnRestaurar.addEventListener("click", function() {
      if (msgDiv) msgDiv.innerHTML = '<span class="spinning" style="color:blue;">Restaurando...</span>';
    });
  }

  var btnBorrar = document.getElementById("btn-borrar");
  if (btnBorrar) {
    btnBorrar.addEventListener("click", function() {
      if (msgDiv) msgDiv.innerHTML = '<span class="spinning" style="color:blue;">Borrando...</span>';
    });
  }

  // Botón para limpiar el tablero (solo frontend)
  var btnLimpiar = document.getElementById("btn-limpiar");
  if (btnLimpiar) {
    btnLimpiar.addEventListener("click", function() {
      document.querySelectorAll('#sudoku-table input[type="text"]').forEach(function(input) {
      input.value = '';
      input.removeAttribute('readonly'); // ¡Permite editar todas las celdas!
      input.className = "cbi-input-text sudoku-inicial"; // Solo la clase base
      });
      if (msgDiv) msgDiv.innerHTML = "&nbsp;";
    });
  }
});
</script>
<%+footer%>

Fichero etc/config/sudoku
Código: [Seleccionar]
config board
    option name 'Everest de Arto Inkala'
    option input '800000000003600000070090200050007000000045700000100030001000068008500010090000400'
    option difficulty 'diabólico'

config board
    option name 'The Imitation Game'
    option input '005300000800000020070010500400005300010070006003200080060500009004000030000009700'
    option difficulty 'diabólico'

config board
    option name 'AI Escargot'
    option input '000000010400000000020000000000050407008000300001090000300400200050100000000806000'
    option difficulty 'diabólico'

config board
    option name 'Diabólico 4'
    option input '000900002050123400000000000000000000000000000000000000000000000003000000000000001'
    option difficulty 'diabólico'

config board
    option name 'Diabólico 5'
    option input '100007090030020008009600500005300900010080002600004000300000010040000007007000300'
    option difficulty 'diabólico'

config board
    option name 'Diabólico 6'
    option input '100000000000003000000020500000000010050407020000000000001000000000000806000000000'
    option difficulty 'diabólico'

config board
    option name 'Diabólico 7'
    option input '000000000000000000000000000000000000000000000000000000000000000000000000000000001'
    option difficulty 'diabólico'

config board
    option name 'Diabólico 8'
    option input '000000000000000000000000000000000000000000000000000000000000000000000000000000009'
    option difficulty 'diabólico'

config board
    option name 'Diabólico 9'
    option input '000000000000000000000000000000000000000000000000000000000000000000000000000000006'
    option difficulty 'diabólico'

config board
    option name 'Diabólico 10'
    option input '000000000000000000000000000000000000000000000000000000000000000000000000000000007'
    option difficulty 'diabólico'

Fichero control. Su nombre lo dice todo; sirve para llevar el control de las versiones del paquete generado. Va en la raiz de la carpeta que soporta toda la estructura del paquete. Es muy importante que su última línea esté en blanco.
Código: [Seleccionar]
Package: luci-app-sudoku
Version: 1.00
Description: Sudoku solver in Lua - just for fun, learning, or because it can.
Maintainer: NOBODY <abandon@ware.com>
Architecture: all
Depends: luci-compat, lua


Por último el fichero ipk_builder.sh. es un script en bash (hay que hacerlo ejecutable) que lanza el proceso automatizado de construcción del paquete ipk y subida e instalación , en el router.
Código: [Seleccionar]
#!/bin/bash

# Configuración
ROUTER_IP="192.168.1.5"
USER="root"
CONTROL="control"

# Extrae nombre y versión del paquete
PKG=$(grep '^Package:' $CONTROL | awk '{print $2}')
VER=$(grep '^Version:' $CONTROL | awk '{print $2}')
IPK="${PKG}_${VER}.ipk"

# 1. Empaqueta el .ipk
tar czf data.tar.gz --owner=0 --group=0 -C . etc usr
tar czf control.tar.gz -C . control
echo "2.0" > debian-binary
tar czf "$IPK" control.tar.gz data.tar.gz debian-binary

# 2. Copia el .ipk al router
scp "$IPK" $USER@$ROUTER_IP:/tmp/

# 3. Instala el paquete y reinicia el servidor web
ssh $USER@$ROUTER_IP "opkg install --force-reinstall --force-overwrite /tmp/$IPK && /etc/init.d/uhttpd restart"

# 4. Limpieza local opcional
rm control.tar.gz data.tar.gz debian-binary

echo "¡Paquete $IPK generado, transferido, instalado (forzando sobreescritura) y servicio reiniciado en $ROUTER_IP!"

Después de construir un primer paquete con el SDK de OpenWrt, me di cuenta que un paquete ipk es, esencialmente, un conjunto de archivos comprimidos. Y para eso ya tenemos el compresor tar de Linux.

No sé si todo este rollo interesa o no, pero al menos yo me he despachado a gusto.

« Última modificación: 27-05-2025, 20:22 (Martes) por raphik »

Desconectado Hwagm

  • Administrador
  • *
  • Mensajes: 18302
Re:Base práctica de desarrollo
« Respuesta #64 en: 27-05-2025, 20:29 (Martes) »
 ;D