[{"content":"Until recently, my heating control system relied on a combination of old \u0026ldquo;dumb\u0026rdquo; electric heaters, Oregon temperature sensors, and Chacon on/off modules. Everything communicated over 433 MHz using homemade RFLink gateways running my RFLink2MQTT software on my home made box.\nOn the software side, I had implemented a custom control system based on target temperatures (Comfort, Eco and Away modes), current room temperature, and a small hysteresis of 0.2°C. The logic was simple: switch heaters on and off to maintain the desired temperature. It worked surprisingly well and served me reliably for many years.\nWhile the software side has proven to be robust, the hardware started to show its age:\nOregon temperature sensor readings are not always received reliably. The mechanical relays inside Chacon DIO modules tend to fail after three to four years of continuous use. My heaters are now more than 15 years old. Besides their poor cosmetic condition, they lack the comfort and thermal inertia offered by modern electric heaters. As I recently had to replace several heaters, I decided it was the perfect opportunity to redesign the whole system. The new version combines modern Wi-Fi temperature sensors, pilot-wire capable heaters, and OpenHAB automation while keeping the same philosophy: simple, reliable and fully automated heating control.\nThis is the story of my refreshed heating system\u0026hellip;\nApproach Problem to solve Unfortunately, replacing the heaters also meant redesigning the control system in OpenHAB.\nModern electric heaters contain much more electronics than older models, and it is becoming increasingly difficult to find units that behave correctly when their power supply is simply switched off and back on. Most of them require user interaction before resuming operation, such as setting the date and time. Some even emit an annoying \u0026ldquo;beep\u0026rdquo; every time power is restored.\nAs a result, cutting the mains power was no longer a viable control method. I needed a way to drive modern heaters while keeping them permanently powered, without relying on any cloud-based solution.\nAnother issue is that I have never found a heater with an integrated thermostat capable of maintaining a truly accurate room temperature. When the weather becomes very cold, the actual temperature often ends up below the configured target. For this reason, I wanted to keep my OpenHAB control logic based on external temperature sensors and a hysteresis algorithm.\nAt the same time, I wanted to move away from my aging 433 MHz infrastructure. While it served me well for years, a Wi-Fi based solution would provide better reliability, easier integration, and one less custom hardware platform.\nThe concept My goal was to keep the same temperature + hysteresis approach that had worked well for years, but replace the crude on/off power switching with a more subtle control mechanism based on the pilot wire available on modern electric heaters.\nIn my setup, I use three temperature profiles:\nEco / Low: when nobody is using the room or during the night Comfort / High: when the room is occupied during normal daytime hours Away: when the house is unoccupied for more than one day The idea is to continue using external temperature sensors while driving the heater through its pilot wire. Depending on the measured temperature and the selected target profile, OpenHAB switches the heater between the following states:\nStop: when the room temperature exceeds the target temperature plus the hysteresis value Comfort: when the room temperature is outside the hysteresis range and the selected profile is Comfort / High Eco: same logic as Comfort, but for the Eco / Low profile This approach combines the best of both worlds. OpenHAB remains responsible for room temperature regulation using accurate external sensors, while the heater itself can still use its built-in control logic and optimized heating modes.\nThe heaters are never completely powered off, and with properly configured Comfort and Eco temperatures, pilot-wire mode changes occur relatively infrequently. This results in a more elegant and reliable system while preserving the temperature control accuracy of the original setup.\nImplementation Temperature Sensor: Shelly Plus H\u0026amp;T To replace my aging 433 MHz Oregon sensors, I chose Shelly Plus H\u0026amp;T devices. They provide temperature and humidity measurements over Wi-Fi and provide a simple yet nice display.\nMy initial goal was to run them on batteries, just like the Oregon sensors. Unfortunately, when battery-powered, the Shelly Plus H\u0026amp;T only reports new values every two hours, or when the temperature changes by more than 0.5°C. This is perfectly acceptable for monitoring, but not for heating control.\nThe problem appears when the temperature changes slowly. For example, if the temperature changes by less than 0.5°C every few minutes, the sensor may wait nearly two hours before reporting a new value. The resulting gap between the actual and reported temperatures is simply too large for accurate heater control.\nI therefore tried the USB-powered trick described here.\nThe idea is to force the sensor to report itself as externally powered, resulting in much more frequent updates. It worked very well, but battery life dropped to only three or four weeks, far from the six to eight months I used to get with my Oregon sensors.\nIn the end, I decided to power all Shelly sensors from USB.\nFor OpenHAB integration, I first experimented with the official Shelly binding, which relies on WebSockets. While it works, I eventually switched to MQTT. MQTT is easier to debug, easier to monitor, and avoids introducing additional complexity into my k3s-based infrastructure.\nBelow is an example of the sensor configuration:\nThing mqtt:topic:shellyplusht-1 \u0026#34;shellyplusht-1\u0026#34; (mqtt:broker:mybroker) [ availabilityTopic=\u0026#34;shellyplusht-1/online\u0026#34;, payloadAvailable=\u0026#34;true\u0026#34;, payloadNotAvailable=\u0026#34;false\u0026#34; ] { Channels: Type number : temperature \u0026#34;temperature\u0026#34; [ stateTopic=\u0026#34;shellyplusht-1/status/temperature:0\u0026#34;, transformationPattern=\u0026#34;JSONPATH($.tC)\u0026#34;] Type number : humidity \u0026#34;humidity\u0026#34; [ stateTopic=\u0026#34;shellyplusht-1/status/humidity:0\u0026#34; , transformationPattern=\u0026#34;JSONPATH($.rh)\u0026#34;] Type number : batteryLevel \u0026#34;batteryLevel\u0026#34; [ stateTopic=\u0026#34;shellyplusht-1/status/devicepower:0\u0026#34;, transformationPattern=\u0026#34;JSONPATH($.battery.percent)\u0026#34;] Type number : wifiSignal \u0026#34;wifiSignal\u0026#34; [ stateTopic=\u0026#34;shellyplusht-1/status/wifi:0\u0026#34; , transformationPattern=\u0026#34;JSONPATH($.rssi)∩JS(rssi2signalstrengh.js)\u0026#34;] The rssi2signalstrength.js transformation converts the Wi-Fi RSSI value (in dBm) into a more user-friendly signal strength indicator based on the Metageek reference scale:\n(function(i) { rssiValue = parseInt(i) if(rssiValue \u0026gt;= -30) return 4; if(rssiValue \u0026gt;= -67) return 3; if(rssiValue \u0026gt;= -70) return 2; if(rssiValue \u0026gt;= -80) return 1; return 0 })(input) Finally, here is an example of the corresponding OpenHAB items. Note that the Sensor_Updt item is used as a timestamp indicating when the last update was received from the device:\nNumber Sensor_Temp \u0026#34;Temperature [%.1f °C]\u0026#34; \u0026lt;temperature\u0026gt; [\u0026#34;Temperature\u0026#34;] { channel=\u0026#34;mqtt:topic:shellyplusht-1:temperature\u0026#34; } Number Sensor_Hum \u0026#34;Humidity [%d %%]\u0026#34; \u0026lt;humidity\u0026gt; [\u0026#34;Humidity\u0026#34;] { channel=\u0026#34;mqtt:topic:shellyplusht-1:humidity\u0026#34; } Number Sensor_Batt \u0026#34;Battery [%d %%]\u0026#34; \u0026lt;batterylevel\u0026gt; [\u0026#34;Level\u0026#34;] { channel=\u0026#34;mqtt:topic:shellyplusht-1:batteryLevel\u0026#34; } Number Sensor_Wifi \u0026#34;Signal wifi [%d]\u0026#34; \u0026lt;qualityofservice\u0026gt; [\u0026#34;Level\u0026#34;] { channel=\u0026#34;mqtt:topic:shellyplusht-1:wifiSignal\u0026#34; } DateTime Sensor_Updt \u0026#34;Last update [%1$ta %1$tR]\u0026#34; \u0026lt;time\u0026gt; [\u0026#34;Timestamp\u0026#34;] { channel=\u0026#34;mqtt:topic:shellyplusht-1:temperature\u0026#34;[profile=\u0026#34;system:timestamp-update\u0026#34;] } Heater Driving with Shelly PM Mini and Shelly Plus 2PM The pilot-wire control concept is well known in Europe and extensively documented on the Internet. I simply never had a reason to look into it before, as my previous on/off based solution worked well enough for many years.\nThe idea is to use a pair of diodes together with a Shelly Plus 2PM to generate the four standard pilot-wire modes supported by most \u0026ldquo;modern\u0026rdquo; electric heaters:\nComfort Eco Stop Frost Protection (Anti-Freeze) To monitor the actual heater power consumption, I also installed a Shelly PM Mini Gen3. This is necessary because the power reported by the Shelly Plus 2PM only reflects the consumption of the pilot-wire control circuit, not the heater itself.\nBelow are some real-world installation photos. One thing I particularly like about this setup is how compact it is. The Shelly Plus 2PM Mini, Shelly PM Mini Gen3 and the required wiring all fit inside a standard wall box using only three heater wires: phase, neutral and pilot wire.\nWith the way I wired the diodes, the pilot-wire modes are mapped to the Shelly Plus 2PM outputs as follows:\nMode SW1 SW2 Comfort Off Off Eco On On Frost Protection On Off Stop Off On While implementing this, I discovered an interesting inconsistency in the Shelly MQTT API. According to the official documentation the status and command topics do not use exactly the same format.\nThe status of an output is published on: \u0026lt;device-name\u0026gt;/status/switch:0\nThe payload is a JSON document containing an output key whose value is either true or false.\nCommands, on the other hand, are sent to: \u0026lt;device-name\u0026gt;/command/switch:0 using the lowercase strings on and off.\nAs a result, retrieving the current state requires a combination of a JSONPath extraction and a transformation map. Likewise, commands must be converted to lowercase because OpenHAB sends ON/OFF commands in uppercase by default.\nBelow are examples of the Thing and Channel definitions:\nThing mqtt:topic:shellyplus2pm-1 \u0026#34;shellyplus2pm-4\u0026#34; (mqtt:broker:mosquitto) [ availabilityTopic=\u0026#34;shellyplus2pm-1/online\u0026#34;, payloadAvailable=\u0026#34;true\u0026#34;, payloadNotAvailable=\u0026#34;false\u0026#34; ] { Channels: Type string : update \u0026#34;update\u0026#34; [ stateTopic=\u0026#34;shellyplus2pm-1/status/sys\u0026#34;, transformationPattern=\u0026#34;JSONPATH($.available_updates)\u0026#34;] Type switch : switch0 \u0026#34;switch 0\u0026#34; [ stateTopic=\u0026#34;shellyplus2pm-1/status/switch:0\u0026#34;, transformationPattern=\u0026#34;JSONPATH($.output)∩MAP(true-false-on-off.map)\u0026#34;, on=\u0026#34;on\u0026#34;, off=\u0026#34;off\u0026#34;, commandTopic=\u0026#34;shellyplus2pm-1/command/switch:0\u0026#34;] Type switch : switch1 \u0026#34;switch 1\u0026#34; [ stateTopic=\u0026#34;shellyplus2pm-1/status/switch:1\u0026#34;, transformationPattern=\u0026#34;JSONPATH($.output)∩MAP(true-false-on-off.map)\u0026#34;, on=\u0026#34;on\u0026#34;, off=\u0026#34;off\u0026#34;, commandTopic=\u0026#34;shellyplus2pm-1/command/switch:1\u0026#34;] } The true-false-on-off.map transformation file looks like this:\ntrue=on false=off on=true off=false And here is an example of the corresponding OpenHAB items:\nGroup Heater_Parents \u0026#34;Chauffage Parents\u0026#34; \u0026lt;radiator\u0026gt; (lChParents,gHeater) [\u0026#34;HVAC\u0026#34;] Switch Heater_Parents_State \u0026#34;Contrôle\u0026#34; \u0026lt;switch\u0026gt; (Heater_Parents,gHeaterSwitchPilot) [\u0026#34;RadiatorControl\u0026#34;] Switch Heater_Parents_Conf \u0026#34;Température\u0026#34; \u0026lt;switch\u0026gt; (Heater_Parents,gHeaterConf) [\u0026#34;RadiatorControl\u0026#34;] Number Heater_Parents_TempEco \u0026#34;Température ECO [%.1f °C]\u0026#34; \u0026lt;temperature\u0026gt; (Heater_Parents,gHeaterTempEco) [\u0026#34;Control\u0026#34;] {widget=\u0026#34;oh-stepper\u0026#34;[step=\u0026#34;0.5\u0026#34;,min=\u0026#34;10\u0026#34;,max=\u0026#34;25\u0026#34;,enableInput=\u0026#34;true\u0026#34;,autorepeat=\u0026#34;true\u0026#34;]} Number Heater_Parents_TempConf \u0026#34;Température CONF [%.1f °C]\u0026#34; \u0026lt;temperature\u0026gt; (Heater_Parents,gHeaterTempConf) [\u0026#34;Control\u0026#34;] {widget=\u0026#34;oh-stepper\u0026#34;[step=\u0026#34;0.5\u0026#34;,min=\u0026#34;10\u0026#34;,max=\u0026#34;25\u0026#34;,enableInput=\u0026#34;true\u0026#34;,autorepeat=\u0026#34;true\u0026#34;]} Number Heater_Parents_TempTarg \u0026#34;Température Cible [%.1f °C]\u0026#34; \u0026lt;temperature\u0026gt; (Heater_Parents,gHeaterTempTarg) [\u0026#34;Control\u0026#34;] {widget=\u0026#34;oh-stepper\u0026#34;[step=\u0026#34;0.5\u0026#34;,min=\u0026#34;10\u0026#34;,max=\u0026#34;25\u0026#34;,enableInput=\u0026#34;true\u0026#34;,autorepeat=\u0026#34;true\u0026#34;]} Number Heater_Parents_Power \u0026#34;Conso actuelle [%.3f Wh]\u0026#34; \u0026lt;energy\u0026gt; (Heater_Parents) [\u0026#34;Measurement\u0026#34;] { channel=\u0026#34;mqtt:topic:shellypmminig3-4:currentpower\u0026#34; } Number Heater_Parents_DayPower \u0026#34;Conso cumulée [%.3f Wh]\u0026#34; \u0026lt;energy\u0026gt; (Heater_Parents,gHeaterDayPower) [\u0026#34;Measurement\u0026#34;] { channel=\u0026#34;mqtt:topic:shellypmminig3-4:totalpower\u0026#34; } Number Heater_Parents_HiPower \u0026#34;Conso heures pleines [%.3f Wh]\u0026#34; \u0026lt;energy\u0026gt; (Heater_Parents,gHeaterPower ) [\u0026#34;Measurement\u0026#34;] Number Heater_Parents_LoPower \u0026#34;Conso heures creuses [%.3f Wh]\u0026#34; \u0026lt;energy\u0026gt; (Heater_Parents,gHeaterPower ) [\u0026#34;Measurement\u0026#34;] Switch Heater_Parents_Reset \u0026#34;RaZ conso\u0026#34; \u0026lt;switch\u0026gt; (Heater_Parents,gHeaterPowerReset) [\u0026#34;RadiatorControl\u0026#34;] { channel=\u0026#34;mqtt:topic:shellypmminig3-4:resetcounter\u0026#34; } Number Heater_Parents_PilotCtrl \u0026#34;Mode radiateur\u0026#34; \u0026lt;switch\u0026gt; (Heater_Parents,gHeaterPilotWire) [\u0026#34;RadiatorControl\u0026#34;] Switch Heater_Parents_Pilot0 \u0026#34;Pilote 1\u0026#34; \u0026lt;switch\u0026gt; (Heater_Parents) [\u0026#34;RadiatorControl\u0026#34;] { channel=\u0026#34;mqtt:topic:shellyplus2pm-4:switch0\u0026#34; } Switch Heater_Parents_Pilot1 \u0026#34;Pilote 2\u0026#34; \u0026lt;switch\u0026gt; (Heater_Parents) [\u0026#34;RadiatorControl\u0026#34;] { channel=\u0026#34;mqtt:topic:shellyplus2pm-4:switch1\u0026#34; } Finally, the heater control logic itself is implemented using the following OpenHAB rules:\nimport org.openhab.core.model.script.ScriptServiceUtil // *************************************************** // SETTINGS // *************************************************** val HEATER_PILOTWIRE_CONF = 0 val HEATER_PILOTWIRE_ECO = 1 val HEATER_PILOTWIRE_FRP = 2 val HEATER_PILOTWIRE_STOP = 3 val HEATER_PILOTWIRE_TEMP = 19 val HEATER_PREFIX=\u0026#34;Heater\u0026#34; val HEATER_STATE_SUFFIX=\u0026#34;State\u0026#34; val HEATER_PILOT0_SUFFIX=\u0026#34;Pilot0\u0026#34; val HEATER_PILOT1_SUFFIX=\u0026#34;Pilot1\u0026#34; val HEATER_TARGETTEMP_SUFFIX=\u0026#34;TempTarg\u0026#34; val HEATER_PILOTWIRE_SUFFIX=\u0026#34;PilotCtrl\u0026#34; val HEATER_HIPOWER_SUFFIX=\u0026#34;HiPower\u0026#34; val HEATER_LOPOWER_SUFFIX=\u0026#34;LoPower\u0026#34; // *************************************************** // FUNCTIONS // *************************************************** val resetHeaterPower = [ | gHeaterPowerReset.members.forEach[ GenericItem item | item.sendCommand(ON) ] ] // *************************************************** // RULES // *************************************************** rule \u0026#34;Chauffage - gestion fil pilote\u0026#34; when Member of gHeaterPilotWire changed then // get pilot wire order : must be within 0..3, else nothing will happen val int orderValue = (triggeringItem.state as Number).intValue // extract: prefix (0), name (1), suffix (3) val roomName = triggeringItemName.split(\u0026#39;_\u0026#39;).get(1) try { val pilot0Item = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + \u0026#34;_\u0026#34;+ roomName +\u0026#34;_\u0026#34; + HEATER_PILOT0_SUFFIX) val pilot1Item = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + \u0026#34;_\u0026#34;+ roomName +\u0026#34;_\u0026#34; + HEATER_PILOT1_SUFFIX) switch (orderValue) { case HEATER_PILOTWIRE_CONF: { pilot0Item.sendCommand(OFF) pilot1Item.sendCommand(OFF) } case HEATER_PILOTWIRE_ECO: { pilot0Item.sendCommand(ON) pilot1Item.sendCommand(ON) } case HEATER_PILOTWIRE_FRP: { pilot0Item.sendCommand(ON) pilot1Item.sendCommand(OFF) } case HEATER_PILOTWIRE_STOP: { pilot0Item.sendCommand(OFF) pilot1Item.sendCommand(ON) } } } catch(ItemNotFoundException e) { logError(\u0026#34;rules\u0026#34;, \u0026#34;Heaters - function setPilotWire - could not get pilot item for: \u0026#34;+ roomName) } end rule \u0026#34;Chauffage - Arret ou mode ECO / CONF selon la temperature cible\u0026#34; when Member of gHeaterSwitchPilot changed then // extract prefix (0), name (1), suffix (3) val roomName = triggeringItemName.split(\u0026#39;_\u0026#39;).get(1) try{ val pilotWireItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + \u0026#34;_\u0026#34;+ roomName +\u0026#34;_\u0026#34; + HEATER_PILOTWIRE_SUFFIX) if (triggeringItem.state == OFF) { pilotWireItem.postUpdate(HEATER_PILOTWIRE_STOP) } else { val targetTempItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + \u0026#34;_\u0026#34;+ roomName +\u0026#34;_\u0026#34; + HEATER_TARGETTEMP_SUFFIX) val targetTemp = (targetTempItem.state as Number).floatValue // Set heater mode according to target temp if(targetTemp \u0026lt; HEATER_PILOTWIRE_TEMP) { pilotWireItem.postUpdate(HEATER_PILOTWIRE_ECO) } else { pilotWireItem.postUpdate(HEATER_PILOTWIRE_CONF) } } } catch(ItemNotFoundException e) { logError(\u0026#34;rules\u0026#34;, \u0026#34;Heaters - rules ECO/CONF - could not get item for: \u0026#34;+ roomName) } end rule \u0026#34;Chauffage - Adaptation file pilote eco / conf selon le changement de temperature cible\u0026#34; when Member of gHeaterTempTarg changed then // extract prefix (0), name (1), suffix (3) val roomName = triggeringItemName.split(\u0026#39;_\u0026#39;).get(1) try { val heaterStateItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + \u0026#34;_\u0026#34;+ roomName +\u0026#34;_\u0026#34; + HEATER_STATE_SUFFIX) if(heaterStateItem.state == ON \u0026amp;\u0026amp; heaterStateItem.getGroupNames.contains(\u0026#34;gHeaterSwitchPilot\u0026#34;)) { val pilotWireItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + \u0026#34;_\u0026#34;+ roomName +\u0026#34;_\u0026#34; + HEATER_PILOTWIRE_SUFFIX) val targetTempItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + \u0026#34;_\u0026#34;+ roomName +\u0026#34;_\u0026#34; + HEATER_TARGETTEMP_SUFFIX) val targetTemp = (targetTempItem.state as Number).floatValue // Set heater mode according to target temp if(targetTemp \u0026lt; HEATER_PILOTWIRE_TEMP) { pilotWireItem.postUpdate(HEATER_PILOTWIRE_ECO) } else { pilotWireItem.postUpdate(HEATER_PILOTWIRE_CONF) } } } catch(ItemNotFoundException e) { logError(\u0026#34;rules\u0026#34;, \u0026#34;Heaters - rules auto adapt pilot wire uppon targettemp changed - could not get item for: \u0026#34;+ roomName) } end rule \u0026#34;Chauffage - RaZ conso en cours\u0026#34; when Time cron \u0026#34;0 0 7 * * ? *\u0026#34; or Time cron \u0026#34;0 0 23 * * ? *\u0026#34; or Time cron \u0026#34;0 0 0 * * ? *\u0026#34; then resetHeaterPower.apply() end rule \u0026#34;Chauffage - RaZ conso heures pleines / creuses\u0026#34; when Time cron \u0026#34;0 0 0 * * ? *\u0026#34; then gHeaterPower.members.forEach[ GenericItem item | item.postUpdate(0) ] end rule \u0026#34;Chauffage - Dispatching conso heures pleines / creuses \u0026#34; when Member of gHeaterDayPower changed then // ignore in case it has been reset if( (triggeringItem.state as Number).intValue == 0 ) { return } // extract prefix (0), name (1), suffix (3) val roomName = triggeringItemName.split(\u0026#39;_\u0026#39;).get(1) try { var GenericItem powerItem // LoPower between 23h and 7h, else HiPower if(now.getHour() \u0026gt; 23 || now.getHour() \u0026lt; 7) { powerItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + \u0026#34;_\u0026#34;+ roomName +\u0026#34;_\u0026#34; + HEATER_LOPOWER_SUFFIX) } else { powerItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + \u0026#34;_\u0026#34;+ roomName +\u0026#34;_\u0026#34; + HEATER_HIPOWER_SUFFIX) } powerItem.state = triggeringItem.state } catch(ItemNotFoundException e) { logError(\u0026#34;rules\u0026#34;, \u0026#34;Heaters - rules Hi/Lo power dispatching - could not get item for: \u0026#34;+ roomName) } end I\u0026rsquo;m not putting the temperature control logic here as it is too long and contains some room detection logic that would not help for the purpose of this article. But basicaly, it checks the temperature every 2 minutes and change the value of Heater_RoomName_PilotCtrl\nWindow sensors In addition to temperature-based control, I also use window sensors as part of the heating automation logic. While most modern heaters provide some form of \u0026ldquo;open window\u0026rdquo; detection, I have found these built-in mechanisms to be rather unreliable. They usually react too slowly or fail to detect certain situations altogether.\nAs with my old Oregon temperature sensors, my previous window sensors relied on a 433 MHz infrastructure. Since one of the goals of this project was to retire all of that aging hardware, I replaced them with inexpensive Wi-Fi sensors based on the CB3S module (BK7231N).\nOne of the advantages of these devices is that it can be reflashed with a custom firmware through a simple serial connection with BK731Flasher\nThe pinout is documented here. The four pins required for flashing are:\n15 = rx 16 = tx 8 = vcc 9 = gnd 3 = CEN I simply soldered a few Dupont wires to the module, connected it to a USB-to-Serial adapter, and flashed the firmware using the appropriate flashing tool.\nOnce flashed, the sensors can be configured to publish their state directly over MQTT.\nBelow is the configuration I use on my devices:\n{ \u0026#34;vendor\u0026#34;: \u0026#34;Tuya\u0026#34;, \u0026#34;bDetailed\u0026#34;: \u0026#34;0\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;Full Device Name Here\u0026#34;, \u0026#34;model\u0026#34;: \u0026#34;enter short model name here\u0026#34;, \u0026#34;chip\u0026#34;: \u0026#34;BK7231N\u0026#34;, \u0026#34;board\u0026#34;: \u0026#34;TODO\u0026#34;, \u0026#34;flags\u0026#34;: \u0026#34;1024\u0026#34;, \u0026#34;keywords\u0026#34;: [ \u0026#34;TODO\u0026#34;, \u0026#34;TODO\u0026#34;, \u0026#34;TODO\u0026#34; ], \u0026#34;pins\u0026#34;: { \u0026#34;7\u0026#34;: \u0026#34;Btn;0\u0026#34;, \u0026#34;8\u0026#34;: \u0026#34;DoorSnsrWSleep_nPup;0\u0026#34;, \u0026#34;14\u0026#34;: \u0026#34;BAT_Relay;0\u0026#34;, \u0026#34;23\u0026#34;: \u0026#34;BAT_ADC;0\u0026#34;, \u0026#34;26\u0026#34;: \u0026#34;WifiLED_n;0\u0026#34; }, \u0026#34;command\u0026#34;: \u0026#34;DSEdge 1\u0026#34;, \u0026#34;image\u0026#34;: \u0026#34;https://obrazki.elektroda.pl/YOUR_IMAGE.jpg\u0026#34;, \u0026#34;wiki\u0026#34;: \u0026#34;https://www.elektroda.com/rtvforum/topic_YOUR_TOPIC.html\u0026#34; } OpenHAB Thing definition is shown below:\nThing mqtt:topic:window_sensor1 \u0026#34;window sensor 1\u0026#34; (mqtt:broker:mosquitto) [ availabilityTopic=\u0026#34;window_sensor1/connected\u0026#34;, payloadAvailable=\u0026#34;online\u0026#34;, payloadNotAvailable=\u0026#34;offline\u0026#34; ] { Channels: Type number : state \u0026#34;State\u0026#34; [ stateTopic=\u0026#34;window_sensor1/0/get\u0026#34;, on=\u0026#34;1\u0026#34;, off=\u0026#34;0\u0026#34; ] Type number : voltage \u0026#34;Voltage\u0026#34; [ stateTopic=\u0026#34;window_sensor1/voltage/get\u0026#34; ] Type number : battery \u0026#34;Battery\u0026#34; [ stateTopic=\u0026#34;window_sensor1/battery/get\u0026#34; ] Type string : ip \u0026#34;IP\u0026#34; [ stateTopic=\u0026#34;window_sensor1/ip\u0026#34; ] Type number : uptime \u0026#34;Uptime\u0026#34; [ stateTopic=\u0026#34;window_sensor1/uptime\u0026#34; ] Type number : freeheap \u0026#34;Freeheap\u0026#34; [ stateTopic=\u0026#34;window_sensor1/freeheap\u0026#34; ] Type number : rssi \u0026#34;Rssi\u0026#34; [ stateTopic=\u0026#34;window_sensor1/rssi\u0026#34;, transformationPattern=\u0026#34;SCALE(rssi.scale)\u0026#34;] Type number : sockets \u0026#34;Sockets\u0026#34; [ stateTopic=\u0026#34;window_sensor1/sockets\u0026#34; ] Type string : ssid \u0026#34;SSID\u0026#34; [ stateTopic=\u0026#34;window_sensor1/ssid\u0026#34; ] Type number : Temperature \u0026#34;Temperature\u0026#34; [ stateTopic=\u0026#34;window_sensor1/temp\u0026#34; ] Type string : mac \u0026#34;MAC addr\u0026#34; [ stateTopic=\u0026#34;window_sensor1/mac\u0026#34; ] Type string : build \u0026#34;Build\u0026#34; [ stateTopic=\u0026#34;window_sensor1/build\u0026#34; ] Type string : host \u0026#34;Hostname\u0026#34; [ stateTopic=\u0026#34;window_sensor1/host\u0026#34; ] } And finally, here is an example Item definition:\nGroup:Switch Window_Parents \u0026#34;Fenêtre parents\u0026#34; \u0026lt;mywindow\u0026gt; (lChParents,gWindow) [\u0026#34;Window\u0026#34;] Switch Window_Parents_State \u0026#34;Etat\u0026#34; \u0026lt;mywindow\u0026gt; (Window_Parents,gWindowState) [\u0026#34;OpenState\u0026#34;] { channel=\u0026#34;mqtt:topic:window_sensor1:state\u0026#34;[profile=\u0026#34;transform:MAP\u0026#34;, function=\u0026#34;on-off-1-0.map\u0026#34;]} Number Window_Parents_Batt \u0026#34;Batterie [%d %%]\u0026#34; \u0026lt;batterylevel\u0026gt; (Window_Parents,gWindowState,gWindowBatt) [\u0026#34;Level\u0026#34;] { channel=\u0026#34;mqtt:topic:window_sensor1:battery\u0026#34; } Number Window_Parents_Wifi \u0026#34;Signal wifi [%d]\u0026#34; \u0026lt;qualityofservice\u0026gt; (Window_Parents,gWindowWifi) [\u0026#34;Level\u0026#34;] { channel=\u0026#34;mqtt:topic:window_sensor1:rssi\u0026#34; } Using dedicated window sensors allows OpenHAB to immediately stop heating when a window is opened and restore the appropriate heating mode when it is closed again. This approach has proven to be significantly more reliable than relying on the heater\u0026rsquo;s built-in open-window detection logic.\nSitemap With all sensors and heater controls integrated into OpenHAB, creating a user-friendly interface becomes straightforward.\nBelow is an example sitemap representing a single room. It provides quick access to the most relevant information and controls:\nCurrent temperature and humidity Heater operating mode Target temperature profile Window status Heater power consumption Sensor health and connectivity information Text item=Sensor_Garage_Temp label=\u0026#34;Garage [%.1f °C]\u0026#34; { Text\titem=Sensor_Garage_Hum Switch\titem=Window_Garage_State label=\u0026#34;Fenêtre\u0026#34;\tmappings=[ON=\u0026#34; Ouverte \u0026#34; , OFF=\u0026#34; Fermée \u0026#34;] Frame label=\u0026#34;Radiateur\u0026#34; { Switch item=Heater_Garage_State\tmappings=[ON=\u0026#34; Marche \u0026#34; , OFF=\u0026#34; Arret \u0026#34;] Switch item=Heater_Garage_Auto\tmappings=[ON=\u0026#34; Auto \u0026#34;\t, OFF=\u0026#34; Manuel \u0026#34;\t] Switch item=Heater_Garage_Conf\tmappings=[ON=\u0026#34; Confort \u0026#34; , OFF=\u0026#34;Economie\u0026#34;\t] } Frame label=\u0026#34;Réglages températures\u0026#34; { Setpoint item=Heater_Garage_TempEco\tminValue=14 maxValue=23 step=0.5 Setpoint item=Heater_Garage_TempConf minValue=14 maxValue=23 step=0.5 Setpoint item=Heater_Garage_TempTarg minValue=14 maxValue=23 step=0.5 } Frame label=\u0026#34;Consommation chauffage\u0026#34; { Text\titem=Heater_Garage_Power Text\titem=Heater_Garage_DayPower } Frame label=\u0026#34;Sonde température\u0026#34; { Text\titem=Sensor_Garage_Wifi Text\titem=Sensor_Garage_Updt } Frame label=\u0026#34;Sonde fenêtre\u0026#34; { Text\titem=Window_Garage_Wifi Text\titem=Window_Garage_Batt } } The resulting interface looks like this:\nOne of the advantages of this approach is that all heating-related information is available in a single view. It becomes easy to understand why a heater is currently running (or not), verify that sensors are reporting correctly, and quickly identify any issue with the automation logic.\nWhile the interface itself remains deliberately simple, it provides all the information required to monitor and troubleshoot the heating system without having to dig into logs or OpenHAB internals.\n","permalink":"https://www.bluemind.org/openhab-shelly-pilote-heater/","summary":"\u003cp\u003eUntil recently, my heating control system relied on a combination of old \u0026ldquo;dumb\u0026rdquo; electric heaters, Oregon temperature sensors, and Chacon on/off modules. Everything communicated over 433 MHz using homemade \u003ca href=\"https://rflink.nl/index.php\"\u003eRFLink\u003c/a\u003e gateways running my \u003ca href=\"https://github.com/jit06/RflinkToJsonMqtt\"\u003eRFLink2MQTT\u003c/a\u003e software on my \u003ca href=\"/rflink-mqtt-v2-enhanced-minimized/\"\u003ehome made box\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eOn the software side, I had implemented a custom control system based on target temperatures (Comfort, Eco and Away modes), current room temperature, and a small hysteresis of 0.2°C. The logic was simple: switch heaters on and off to maintain the desired temperature. It worked surprisingly well and served me reliably for many years.\u003c/p\u003e","title":"Driving Electric Heaters with OpenHAB, Pilot Wire and Wi-Fi Sensors"},{"content":"I’ve been using XMG laptops since 2020. For me, they offer one of the best value-for-money options if you want an elegant machine that works equally well for productivity and gaming. After two Fusion models (m19, then m22), I recently switched to a XMG Pro 16 m25.\nCompared to the Fusion, the Pro 16 is a bit heavier, but it comes with a RTX 5070 Ti (12 GB instead of 8 GB) and, more importantly, allows the GPU to draw up to 140 W (versus 110 W on the Fusion). For gaming workloads, this makes a noticeable difference.\nAs usual, I installed Arch Linux and went through a series of tweaks and adjustments to get the most out of the hardware—especially to let the GPU use as much power as possible when needed. I decided to share what I learned, both for people wondering whether this laptop works well under Linux and, more specifically, whether it is a good choice for Linux gaming.\nSpoiler alert: yes—and it’s very good.\nPre-requisites The advanced BIOS settings are factory-locked. As far as I know, the only way to unlock them is to boot Windows 11 once and install the official XMG Control Center application. This tool provides an option to enable advanced BIOS features, which then allows fine-tuning of memory timings as well as CPU voltage (including undervolting).\nIn my opinion, this is quite disappointing. It is actually the only reason I had to install Windows 11 on this laptop. I still find it surprising to see such constraints today, especially when Linux desktop and gaming usage keeps growing and has already proven to be superior in many areas.\nTo enable this advanced mode, you first need to create a custom performance profile in the Performance menu of the Control Center:\nOnce this custom profile is selected, a new option called “CPU Advanced Performance Menu” becomes available. Enabling it immediately unlocks the advanced BIOS options.\nThe good news is that you do not need to keep running Control Center afterward—once enabled, the BIOS remains unlocked permanently.\nMemory My XMG Pro is equipped with 2×16 GB DDR5-6400 CL38 modules with an XMP profile (Kingston KF564S38IBK2-32). However, out of the box, I was surprised to see the memory running at only 4800 MT/s.\nI initially tried enabling XMP1, but I never managed to get a stable boot. In addition, boot times became extremely long—sometimes taking several minutes, which clearly wasn’t acceptable.\nThe best compromise I found was to manually limit the memory speed to 6000 MT/s and select XMP3, which corresponds to 6000 MT/s / CL40. This configuration has proven to be stable and offers a noticeable improvement over the default settings, without the excessive boot delays.\nGetting max power on GPU The concept My primary goal was to achieve the best possible gaming performance. Based on my experience with gaming laptops, the bottleneck is almost always the GPU, while the CPU often sits below 50% load in real-world gaming scenarios.\nTo maximize performance, the GPU needs to operate as close as possible to its maximum power budget (140 W). Achieving this requires a combination of firmware, driver, and power-management tweaks. On this platform, the total power budget is shared between the CPU and the GPU, meaning that limiting CPU power directly benefits GPU headroom. In short: the less power the CPU wastes, the more the GPU can consume.\nMy objective was therefore to find the best balance between CPU efficiency (performance per watt) and maximum sustained GPU power.\nAt a high level, this involved the following steps:\nSetting the system to “Enthusiast Mode” in the BIOS Limiting CPU power to free up thermal and electrical budget Installing the NVIDIA proprietary drivers Switching the system to dGPU-only mode in the BIOS, as Hybrid mode caps GPU power at 115 W A quick note about this last point: I did not find any way to switch GPU modes from Linux itself—even using tools like supergfxctl. From Linux, it is only possible to switch to hybrid, integrated, or VFIO modes. Switching to dGPU-only is only available via the BIOS.\nThat said, this setup still offers the best of both worlds:\nIntegrated GPU mode for battery life Dedicated RTX 5070 mode for gaming, with full power unlocked The only downside is that switching between these modes requires a BIOS reboot—but once configured, it works reliably.\nCPU Undervolting Undervolting on a Core Ultra 9 is fairly complex, mainly because several voltage domains are involved: Core, Ring, and VF points 1 to 6. All of these parameters can be adjusted directly from the BIOS on the XMG Pro 16 m25 once advanced options are unlocked.\nFor each configuration change, I systematically ran two different types of tests:\nFull CPU load (stress test)\nThis was used purely to validate stability. In practice, the CPU always ends up maxing out around 75 W, as it aggressively boosts frequency until it reaches its thermal limit, which appears to be 94 °C on the Pro 16 m25. “Gaming-like” workload\nThis scenario is more representative of real usage. The goal here was to identify the settings that allow the highest sustained frequencies, which translates to maximum compute performance within the thermal and power budget. One important takeaway from this process is that aggressive undervolting is not always beneficial. Even when fully stable, too much undervolting can actually cause the CPU to reduce its operating frequency in order to compensate for the reduced available voltage and power.\nIn other words, stability alone is not a sufficient metric: the best results came from moderate undervolting, where the CPU maintains higher clocks rather than chasing the lowest possible voltage.\nBelow are a few screenshots of the BIOS settings used during this tuning process:\nGPU Unfortunately, there is no straightforward way to undervolt NVIDIA GPUs on Linux. However, overclocking effectively forces the GPU to operate at lower voltages for a given frequency. In practice, this acts like an undervolt: the GPU attempts to reach higher clocks, but within the power and thermal limits, it simply stabilizes at the highest achievable frequency for that voltage.\nTo push this further, I followed guidance from both ArchWiki and other community sources that leverage the NVIDIA API in Python. Using this method, I applied a clock offset of 300 MHz with nvmlDeviceSetGpcClkVfOffset.\nIn simple terms, this means that for any given target frequency, the GPU will now use the voltage of that frequency minus the offset. Effectively, this achieves a mild undervolt, improving efficiency without sacrificing stability or maxing out thermal limits.\nTests As explained earlier, I ran two types of tests: stress tests and in-game benchmarks. The goal was to identify the sweet spot between CPU stability, maximum GPU power, and the highest FPS.\nCPU Stress Test For CPU stress testing, I used: stress-ng --cpu 24 --cpu-method fft --timeout 5m. While the test was running, I monitored three key metrics using Tuxedo Control Center:\nCPU temperature – lower is better, as it frees more thermal headroom for the GPU Maximum stable frequency – higher is better, since higher frequency equals more compute power Power consumption (Watts) – lower is better, because it leaves more power available for the GPU The undervolt values I tested are listed in the following order: Core VF1–VF6 → Ring VF1–VF6.\nUndervolt settings temperature (°C) Stabilized frequency (Ghz) Watts Bios option “Level 2” 87 4.6 90 0 / 50 / 30 / 0 / 20 / 0 – 45 / 45 / 45 / 40 / 40 / 35 85 4.7 85 80 / 70 / 50 / 0 / 30 / 0 – 70 / 70 / 60 / 55 / 55 / 45 85 4.4 85 60 / 50 / 65 / 65 / 30 / 0 – 45 / 45 / 55 / 40 / 40 / 35 crash crash crash 60 / 50 / 50 / 50 / 30 / 0 – 45 / 45 / 50 / 40 / 40 / 35 84 4.7 85 The final values I settled on are those that offered the best balance of stability and performance.\nIn-Game Benchmarks Starting from the optimal CPU configuration, I ran SuperTuxKart at the laptop’s native resolution (2560×1600). The goal was to maximize FPS by adjusting the CPU power budget using powercap-set. Limiting CPU power allows the GPU to consume more watts, which translates directly into higher performance.\nDuring testing, I also monitored GPU power consumption. Although it never hit the theoretical 145 W maximum, it stably hovered between 130–135 W. The best FPS results coincided with these higher GPU power levels.\nFor each scenario, I configured both long-term and short-term CPU power limits, for example: 60 W long-term and 95 W short-term.\nsudo powercap-set intel-rapl-mmio -z 0 -c 0 -l 65000000 sudo powercap-set intel-rapl-mmio -z 0 -c 0 -s 1000000 sudo powercap-set intel-rapl-mmio -z 0 -c 1 -l 95000000 sudo powercap-set intel-rapl-mmio -z 0 -c 1 -s 1000000 Long / short term min average max 60 / 95 113 158 183 60 / 80 132 158 182 60 / 70 130 158 182 50 / 60 135 165 190 The final settings were tuned for SuperTuxKart, but I also tested CPU-heavy games such as The Last of Us Remastered to ensure performance remained optimal.\nOverall, the best balance I found was with CPU power limits of 55 W long-term / 65 W short-term, providing both stability and maximum GPU utilization.\nGamemoderun\nAfter completing all the tests, I decided to leverage gamemoderun to automatically execute scripts when games start and stop. This allows me to apply optimizations such as CPU power limits and GPU undervolting only while gaming, keeping the system in a more conservative state at other times.\nStart Script:\n#!/bin/bash # Limit CPU power to give more for the GPU # longterm : limit to 55w sudo powercap-set intel-rapl-mmio -z 0 -c 0 -l 55000000 sudo powercap-set intel-rapl-mmio -z 0 -c 0 -s 1000000 # short term: allow 65w for 1 seconds sudo powercap-set intel-rapl-mmio -z 0 -c 1 -l 65000000 sudo powercap-set intel-rapl-mmio -z 0 -c 1 -s 1000000 # undervolt the gpu sudo .local/bin/nvidia_undervolt.sh # keyboard color sudo .local/bin/keyboard_gaming.sh End Script:\n#!/bin/bash # restore CPU power limit to default ones sudo powercap-set intel-rapl-mmio -z 0 -c 0 -l 210000000 sudo powercap-set intel-rapl-mmio -z 0 -c 0 -s 55967744 sudo powercap-set intel-rapl-mmio -z 0 -c 1 -l 210000000 sudo powercap-set intel-rapl-mmio -z 0 -c 1 -s 2440 # restore keyboard color sudo .local/bin/keyboard_color.sh Nvidia Undervolt Script:\n#!/usr/bin/env python from pynvml import * nvmlInit() device = nvmlDeviceGetHandleByIndex(0) nvmlDeviceSetGpcClkVfOffset(device,300) These scripts ensure that the system automatically maximizes GPU performance and optimizes CPU power for each gaming session, without requiring manual adjustments.\nMore details about keyboard scripts and automation will be covered in the next chapter.\nMisc / Bonus\nIn this section, I’m sharing some additional tips and insights about using this laptop under Linux. Keep in mind that their relevance or effectiveness may change over time with software updates or new drivers.\nTuxedo control center Arch Linux provides the tuxedo-control-center-bin package via the AUR. In practice, I found it mostly redundant under Linux. While it allows adjusting some CPU settings, I prefer automating everything with scripts and gamemoderun rather than doing manual tweaks.\nThe only situation where it proved useful was for monitoring CPU temperatures and power consumption during benchmarking. That said, there are plenty of alternative tools on Linux that can provide the same information more flexibly.\nBattery profile On Linux, the battery profile resets at each boot. I wanted to set it to “stationary” to help extend battery lifespan.\nThe battery profile is exposed via: /sys/devices/platform/tuxedo_keyboard/charging_profile/charging_profile\nUnfortunately, it cannot be persisted using sysctl.\nFollowing the guidance from the systemd Arch Linux wiki, I created a tmpfiles entry at /etc/tmpfiles.d/tuxedo_settings.conf to automatically apply the desired profile at boot:\n# Path Mode UID GID Age Argument w /sys/devices/platform/tuxedo_keyboard/charging_profile/charging_profile - - - - stationary This ensures that the battery profile is set correctly every time the laptop starts, without manual intervention.\nKeyboard color I wasn’t able to replicate the exact “cool” lighting effects that the XMG Control Center provides on Windows 11. However, under Linux, the keyboard backlight can be controlled like any other LED using the Tuxedo drivers. On Arch Linux, the relevant AUR package is: tuxedo-drivers-nocompatcheck-dkms.\nThe \u0026ldquo;nocompatcheck\u0026rdquo; variant allows installing the driver on non-official Tuxedo hardware, including XMG laptops.\nI then created a set of scripts to dynamically control the keyboard color based on the desktop background or whether GameModeRun is active.\nI’m using Plasma’s “Picture of the Day” feature with the Bing provider for my desktop background. The scripts are stored in ~/.local/bin:\nkeyboard_color.sh – Sets the keyboard color based on the current desktop image. keyboard_gaming.sh – Applies a dedicated color setup for gaming; triggered by gamemoderun wallpaper_check.sh – Runs at Plasma session start, checks for background image changes, and calls keyboard_color.sh if necessary. Below are the three scripts:\nkeyboard_color.sh\n#!/bin/bash # reduce RGB palette to get something more accurate for the LEDs. # only allows 3 value for each component : 0, 122, 255 quantize_rgb_3levels() { local r=$1 g=$2 b=$3 # array with values + names vals=(\u0026#34;$r:R\u0026#34; \u0026#34;$g:G\u0026#34; \u0026#34;$b:B\u0026#34;) # sort IFS=$\u0026#39;\\n\u0026#39; sorted=($(sort -n \u0026lt;\u0026lt;\u0026lt;\u0026#34;${vals[*]}\u0026#34;)) unset IFS declare -A out # the lowest value become 0 c_min=${sorted[0]#*:} out[$c_min]=0 # the medium value become 122 c_mid=${sorted[1]#*:} out[$c_mid]=122 # the greatest value become 255 c_max=${sorted[2]#*:} out[$c_max]=255 echo \u0026#34;${out[R]} ${out[G]} ${out[B]}\u0026#34; } # Find the lastest background image WALLPAPER=$(find ~/.cache/plasma_engine_potd -type f ! -name \u0026#39;*.json\u0026#39; \\ -printf \u0026#39;%T@ %p\\n\u0026#39; \\ | sort -nr \\ | head -n1 \\ | cut -d\u0026#39; \u0026#39; -f2-) [ -f \u0026#34;$WALLPAPER\u0026#34; ] || exit 1 # Use image magick to extract the dominant color read R G B \u0026lt;\u0026lt;\u0026lt; \u0026#34;$(magick \u0026#34;$WALLPAPER\u0026#34; \\ -resize 100x100! \\ -colors 1 \\ -format \u0026#34;%[fx:int(255*r)] %[fx:int(255*g)] %[fx:int(255*b)]\u0026#34; info:-)\u0026#34; read R_Q G_Q B_Q \u0026lt; \u0026lt;(quantize_rgb_3levels \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34;) echo \u0026#34;R=$R_Q G=$G_Q B=$B_Q\u0026#34; #R=$(( R * 35 / 100 )) #B=$(( B * 35 / 100 )) # set leds colors for led in /sys/class/leds/rgb:kbd_backlight*; do printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R_Q\u0026#34; \u0026#34;$G_Q\u0026#34; \u0026#34;$B_Q\u0026#34; \u0026gt; \u0026#34;$led/multi_intensity\u0026#34; \u0026amp; done keyboard_gaming.sh:\n#!/bin/bash # Set which color to use for gaming mode R=255 G=0 B=0 # Set a low intensity white for all keys by default for led in /sys/class/leds/rgb:kbd_backlight*; do printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;9\u0026#34; \u0026#34;20\u0026#34; \u0026#34;9\u0026#34; \u0026gt; \u0026#34;$led/multi_intensity\u0026#34; done # Set the gaming color for special keys # LEFT CTRL printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight/multi_intensity\u0026#34; # LEFT ALT printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_4/multi_intensity\u0026#34; # SPACE printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_7/multi_intensity\u0026#34; # ALT GR #printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_10/multi_intensity\u0026#34; # RIGTH CTRL #printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_12/multi_intensity\u0026#34; # LEFT printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_13/multi_intensity\u0026#34; # UP printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_14/multi_intensity\u0026#34; # RIGTH printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_15/multi_intensity\u0026#34; # DOWN printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_18/multi_intensity\u0026#34; # LEFT SHIFT printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_22/multi_intensity\u0026#34; # A printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_44/multi_intensity\u0026#34; # S printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_45/multi_intensity\u0026#34; # D printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_46/multi_intensity\u0026#34; # W printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_66/multi_intensity\u0026#34; # ENTER printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_77/multi_intensity\u0026#34; # BACK printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_98/multi_intensity\u0026#34; # ESC printf \u0026#34;%d %d %d\\n\u0026#34; \u0026#34;$R\u0026#34; \u0026#34;$G\u0026#34; \u0026#34;$B\u0026#34; \u0026gt; \u0026#34;/sys/class/leds/rgb:kbd_backlight_105/multi_intensity\u0026#34; wallpaper_check.sh:\n#!/bin/bash SCRIPT_RGB=\u0026#34;sudo ~/.local/bin/keyboard_color.sh\u0026#34; # Filter to only get wallpaper change signals dbus-monitor --session \u0026#34;type=\u0026#39;signal\u0026#39;,interface=\u0026#39;org.kde.PlasmaShell\u0026#39;,member=\u0026#39;wallpaperChanged\u0026#39;\u0026#34; | \\ while read -r line; do if echo \u0026#34;$line\u0026#34; | grep -q \u0026#34;wallpaperChanged\u0026#34;; then echo \u0026#34;Wallpaper changed detected!\u0026#34; $SCRIPT_RGB fi done This setup allows the keyboard lighting to adapt dynamically to the desktop or gaming session, giving a visual feedback similar to the Windows XMG Control Center, but fully integrated into Linux.\n","permalink":"https://www.bluemind.org/maxing-xmg-pro-16-m25-linux/","summary":"\u003cp\u003eI’ve been using XMG laptops since 2020. For me, they offer one of the best value-for-money options if you want an elegant machine that works equally well for productivity and gaming. After two Fusion models (m19, then m22), I recently switched to a XMG Pro 16 m25.\u003c/p\u003e\n\u003cp\u003eCompared to the Fusion, the Pro 16 is a bit heavier, but it comes with a RTX 5070 Ti (12 GB instead of 8 GB) and, more importantly, allows the GPU to draw up to 140 W (versus 110 W on the Fusion). For gaming workloads, this makes a noticeable difference.\u003c/p\u003e","title":"XMG Pro 16 m25 under Linux: Maximum GPU Power and Gaming Performance"},{"content":"For several years, I relied on a wall-mounted Android tablet running Fully Kiosk Browser as a control panel for my home automation system (OpenHAB). When my last tablet finally died, I decided to try a different approach: a Raspberry Pi paired with a touchscreen. I had looked into this option a few years ago, but back then the software side required far too many tweaks to match the smooth experience of an Android tablet (especially for touchscreen interactions and on-screen keyboard support).\nFortunately, things have evolved. It’s now surprisingly easy to build a web-based kiosk tablet using a Raspberry Pi and a reasonably large display. One of my main goals was to create a device that needs little to no maintenance while staying permanently available.\nThis is the story of my new Raspberry-Pi-powered wall-mounted tablet…\nUsed Materials Here is the list of components I used to build the wall-mounted tablet:\nRaspberry Pi CM4 (4 GB RAM, Wi-Fi, 16 GB eMMC) 15.6\u0026quot; Full HD HDMI IPS touchscreen (brand “Showscren”) Waveshare Nano Board C (CM4 carrier board) Waveshare aluminium heatsink for Raspberry Pi CM4 Uadme camera module (IMX219 sensor) 30 cm ultra-thin HDMI cable (Thsucords) 25 cm USB-A 90° to USB-C cable 13 cm flat “FPC-style” USB-A to USB-C 90° cable Hardware Assembling The choice of the CM4 combined with the Nano Board C was intentional: I needed a compact setup that could fit inside a standard cavity wall box alongside a 5 V power supply and a heatsink.\nFirst, the heatsink needs to be screwed onto the CM4:\nThen the Nano Board C is connected to the CM4. The assembled unit has a total depth of just 20.3 mm:\nBelow you can see how the tablet is powered and mounted on the wall. The last photo highlights the position of the camera module:\nMounting eMMC to Flash an OS I chose a CM4 with eMMC instead of a microSD card because it is faster and provides sufficient storage for this tablet. However, flashing the eMMC requires a small utility called rpiboot, which essentially exposes the eMMC as a standard block device on your PC (e.g., /dev/sdc).\nThe Waveshare Nano C board features a small boot button that must be set to “ON.” Then, connect the CM4 to your PC via a USB-C cable. Running rpiboot produces the following output:\nRPIBOOT: build-date Jan 31 2022 version 0~20220315+git6fa2ec0+nowin-0ubuntu1 Waiting for BCM2835/6/7/2711... Loading embedded: bootcode4.bin Sending bootcode.bin Successful read 4 bytes Waiting for BCM2835/6/7/2711... Loading embedded: bootcode4.bin Second stage boot server Loading embedded: start4.elf File read: start4.elf Second stage boot server done At this point, the Pi’s LEDs turn on, and the eMMC is accessible as a standard mass storage device, ready for flashing.\nInstall and configure Base install I initially experimented with LineageOS by following the installation guide, hoping to replicate the setup of my previous Android-based tablet running Fully Kiosk Browser. Unfortunately, a few limitations quickly made it unsuitable for this project:\nThe build lacks full camera and Bluetooth support, which makes videoconferencing apps unusable. The browser became unstable and regularly crashed or froze after only a few hours displaying my HabPanel dashboard. Given these constraints, I switched to Raspberry Pi OS with Desktop (the standard edition — not “full” and not “lite”), which offers better stability and broader hardware compatibility for a kiosk-style setup:\nsudo dd if=2025-10-01-raspios-trixie-arm64.img of=/dev/sdc bs=2048 status=progress\nConfiguring Once the system was installed, I completed the initial setup by configuring Wi-Fi, setting Firefox as the default browser, and opening the Control Center for the first adjustments:\nDisabled the background image and set a plain black wallpaper Switched the system theme to Dark Following the Waveshare CM4-Nano-C documentation, I applied the recommended settings in config.txt:\ndtoverlay=imx219,i2c_vc,cam0 set camera_auto_detect=0 I verified camera support using rpicam-hello, as described in the official documentation.\nUsing raspi-config, I enabled a few essential services:\nSSH VNC The on-screen keyboad in automatic mode Desktop adjustments:\nRemoved the trash icon from the desktop Enabled multitouch gestures from the Control Center Firefox configuration:\nDisabled spell checking Disabled recommended extensions and integrated features Disabled data collection In about:config, set dom.w3c_touch_events.enabled to 1 to improve touchscreen handling Finally, I paired my Bluetooth Plantronics Calisto 620 using the SBC-XQ audio codec. Now, whenever I power it on, it automatically connects to the Pi and becomes the default system microphone.\nAutomated upgrade Keeping the OS up to date with automatic reboots is part of the “bare minimum” for a true no-toil device. Since Raspberry Pi OS is based on Debian, I relied on the standard unattended-upgrades service.\nFirst, install the package:\napt-get install unattended-upgrades -y\nThen copy the default configuration and edit your local version:\nsudo cp /etc/apt/apt.conf.d/50unattended-upgrades /etc/apt/apt.conf.d/52unattended-upgrades-local In the edited file, I enabled the following:\nAllow updates and proposed-updates in the Origins-Pattern section Unattended-Upgrade::Remove-Unused-Kernel-Packages Unattended-Upgrade::Remove-New-Unused-Dependencies Unattended-Upgrade::Remove-Unused-Dependencies Unattended-Upgrade::Automatic-Reboot Unattended-Upgrade::Automatic-Reboot-WithUsers Set Unattended-Upgrade::Automatic-Reboot-Time to 04:00 My logic was: download updates at 2:00, apply upgrades at 3:00, and reboot at 4:00 if required. Everything happens overnight, with no disruption during the day.\nTo tune the timers, I edited the systemd units apt-daily.timer\n[Timer] OnCalendar=02:00 RandomizedDelaySec=0 Then apt-daily-upgrade.timer:\n[Timer] OnCalendar=03:00 RandomizedDelaySec=0 Photo frame while idle On my previous Android setup, Fully Kiosk Browser provided a built-in photo slideshow when the tablet was idle. It’s a neat way to make a wall-mounted display useful even when nothing is actively shown. Raspberry Pi OS / Debian doesn’t offer such a feature out of the box, but implementing it turned out to be straightforward.\nI also added a small improvement: automatic random photo selection at a configurable frequency. Photos are copied from a CIFS share hosted on my NAS to the Raspberry Pi, and a script handles the randomization logic before copying them locally.\nThe CIFS share is mounted via fstab using a line similar to:\n//mynas.local/photos /mnt/photos cifs ro,username=user,password=pwd,iocharset=utf8 The script (/usr/local/bin/sync_photos.sh) accepts several parameters:\nThe source path (remote CIFS folder) The local destination folder The maximum number of photos to copy How many recent photos to include How many months define a recent photo How many months define an old photo This may look a bit over-engineered, but I like my photo frame to display a balanced mix of fresh memories and older ones. The script also handles folders to ignore.\nSince the tablet is mounted horizontally, I added a simple check to only keep landscape-oriented photos.\nTo avoid putting unnecessary load on my NAS, the script generates a cached list of all eligible photos based on the selected parameters. This cache is reused until a new file is detected on the NAS, at which point it is rebuilt.\n#!/bin/bash ########################################################################## # Usage ########################################################################## if [ $# -lt 2 ]; then echo \u0026#34;Usage: $0 \u0026lt;source\u0026gt; \u0026lt;destination\u0026gt; [total] [newer] [month_newer] [month_old]\u0026#34; exit 1 fi ########################################################################## # Globals ########################################################################## # set values from mandatory params SRC=\u0026#34;$1\u0026#34; DEST=\u0026#34;$2\u0026#34; # set default values depending on params TOTAL=${3:-50} RECENT_COUNT=${4:-30} RECENT_MONTHS=${5:-6} OLD_MONTHS=${6:-12} # where cached index and last scan date are stored INDEX_DIR=\u0026#34;$HOME\u0026#34; INDEX_FILE=\u0026#34;$INDEX_DIR/_index.txt\u0026#34; LASTSCAN=\u0026#34;$INDEX_DIR/_last_scan.txt\u0026#34; ########################################################################## # Functions ########################################################################## # Index builder rebuild_index() { echo \u0026#34;Building jpg images index...\u0026#34; echo \u0026#34;\u0026#34; \u0026gt; \u0026#34;$INDEX_FILE\u0026#34; find \u0026#34;$SRC\u0026#34; \\ -type d \\( \\ -path \u0026#34;$SRC/temp\u0026#34; -o \\ -path \u0026#34;$SRC/test\u0026#34; -o \\ -path \u0026#34;$SRC/archive\u0026#34; \\ \\) -prune -o \\ -type f \\( -iname \u0026#34;*.jpg\u0026#34; -o -iname \u0026#34;*.jpeg\u0026#34; -o -iname \u0026#34;*.png\u0026#34; \\) -print | while read -r file; do ts=$(stat -c %Y \u0026#34;$file\u0026#34; 2\u0026gt;/dev/null) echo \u0026#34;$ts|$file\u0026#34; \u0026gt;\u0026gt; \u0026#34;$INDEX_FILE\u0026#34; done date +%s \u0026gt; \u0026#34;$LASTSCAN\u0026#34; echo \u0026#34;Index has been rebuilt (\u0026#34;$(wc -l \u0026lt; $INDEX_FILE)\u0026#34; indexed images).\u0026#34; } # copy images with Landscape orientation only copy_with_orientation_check() { local list=\u0026#34;$1\u0026#34; local needed=\u0026#34;$2\u0026#34; local count=0 while read -r file; do [ -z \u0026#34;$file\u0026#34; ] \u0026amp;\u0026amp; continue orientation=$(exiftool -s -s -s -Orientation \u0026#34;$file\u0026#34; 2\u0026gt;/dev/null) if [[ \u0026#34;$orientation\u0026#34; == *\u0026#34;Horizontal\u0026#34;* ]]; then cp \u0026#34;$file\u0026#34; \u0026#34;$DEST/\u0026#34; count=$((count + 1)) fi [ $count -ge $needed ] \u0026amp;\u0026amp; break done \u0026lt;\u0026lt;\u0026lt; \u0026#34;$list\u0026#34; } ########################################################################## # Main logic ########################################################################## # Check wether index need to be rebuilt (new file detected or no index file) needs_rebuild=0 if [ ! -f \u0026#34;$INDEX_FILE\u0026#34; ] || [ ! -f \u0026#34;$LASTSCAN\u0026#34; ]; then needs_rebuild=1 else last_scan_ts=$(cat \u0026#34;$LASTSCAN\u0026#34;) last_scan_date=$(date -d @\u0026#34;$last_scan_ts\u0026#34; +\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;) # search at least one file newer than the latest scan date new_files=$(find \u0026#34;$SRC\u0026#34; -type f -newermt \u0026#34;$last_scan_date\u0026#34; -print -quit 2\u0026gt;/dev/null) if [ -n \u0026#34;$new_files\u0026#34; ]; then needs_rebuild=1 fi fi if [ $needs_rebuild -eq 1 ]; then echo \u0026#34;Index has to be (re)built\u0026#34; rebuild_index else echo \u0026#34;Index not updated - no new images detected\u0026#34; fi # Photos selection now=$(date +%s) recent_cutoff=$((now - RECENT_MONTHS*30*24*3600)) old_cutoff=$((now - OLD_MONTHS*30*24*3600)) mapfile -t recent_list \u0026lt; \u0026lt;(awk -F\u0026#39;|\u0026#39; -v rc=\u0026#34;$recent_cutoff\u0026#34; \u0026#39;$1 \u0026gt;= rc {print $2}\u0026#39; \u0026#34;$INDEX_FILE\u0026#34;) mapfile -t old_list \u0026lt; \u0026lt;(awk -F\u0026#39;|\u0026#39; -v oc=\u0026#34;$old_cutoff\u0026#34; \u0026#39;$1 \u0026lt;= oc {print $2}\u0026#39; \u0026#34;$INDEX_FILE\u0026#34;) # creating random images list recent_candidates=$(printf \u0026#34;%s\\n\u0026#34; \u0026#34;${recent_list[@]}\u0026#34; | shuf -n \u0026#34;$RECENT_COUNT\u0026#34; 2\u0026gt;/dev/null) old_needed=$((TOTAL - RECENT_COUNT)) old_candidates=$(printf \u0026#34;%s\\n\u0026#34; \u0026#34;${old_list[@]}\u0026#34; | shuf -n \u0026#34;$old_needed\u0026#34; 2\u0026gt;/dev/null) # clean-up the destination before copy new images rm -f \u0026#34;$DEST\u0026#34;/* #echo \u0026#34;copie newer images...\u0026#34; copy_with_orientation_check \u0026#34;$recent_candidates\u0026#34; \u0026#34;$RECENT_COUNT\u0026#34; #echo \u0026#34;copy old images...\u0026#34; copy_with_orientation_check \u0026#34;$old_candidates\u0026#34; \u0026#34;$old_needed\u0026#34; echo \u0026#34;Done. Number of copied photos : $(ls \u0026#34;$DEST\u0026#34; | wc -l)\u0026#34; The script is triggered via cron (crontab -e), along with scheduled display power on/off during night hours to save energy. I also restart Firefox every day at 05:30, as HabPanel tends to freeze after several hours.\n30 5 * * * killall firefox; XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=\u0026#34;wayland-0\u0026#34; firefox --kiosk \u0026#34;http://openhab.local.lan/habpanel/index.html#/view/test\u0026#34; \u0026amp; 00 5 * * 6 /usr/local/bin/sync_photos.sh /mnt/photos /home/tablet/Pictures 70 45 6 12 00 1 * * * WAYLAND_DISPLAY=\u0026#34;wayland-0\u0026#34; wlr-randr --output HDMI-A-1 --off 30 6 * * 1,5 WAYLAND_DISPLAY=\u0026#34;wayland-0\u0026#34; wlr-randr --output HDMI-A-1 --on 30 9 * * 6,7 WAYLAND_DISPLAY=\u0026#34;wayland-0\u0026#34; wlr-randr --output HDMI-A-1 --on To display photos, I installed imv (apt get install -y imv).Together with swayidle, the slideshow starts automatically after 30 seconds of inactivity. Example:\nswayidle timeout 30 \u0026#39;imv-wayland -f -s full -t 10 /home/tablet/Pictures/*\u0026#39; resume \u0026#39;killall imv-wayland\u0026#39; Autostart everything at boot\nTo make the tablet fully autonomous, both Firefox (in kiosk mode) and the idle photo slideshow must start automatically at boot. This is handled through standard XDG autostart entries.\nI created two .desktop files in /etc/xdg/autostart/:\nfirefox-kiosk.desktop:\n[Desktop Entry] Name=Firefox kiosk Comment=launch Firefox in kiosk mode with habpanel Exec=firefox --kiosk \u0026#34;http://openhab.local.lan/habpanel/index.html#/view/test\u0026#34; Terminal=false Type=Application idle-slideshow.desktop\n[Desktop Entry] Name=Idle-slideshow Comment=Display photos from user Pictures directory when idle Exec=swayidle timeout 30 \u0026#39;imv-wayland -f -s full -t 10 $HOME/Pictures/*\u0026#39; resume \u0026#39;killall imv-wayland\u0026#39; Terminal=false Type=Application ","permalink":"https://www.bluemind.org/pi-wall-mount-tablet/","summary":"\u003cp\u003eFor several years, I relied on a wall-mounted Android tablet running Fully Kiosk Browser as a control panel for my home automation system (OpenHAB). When my last tablet finally died, I decided to try a different approach: a Raspberry Pi paired with a touchscreen. I had looked into this option a few years ago, but back then the software side required far too many tweaks to match the smooth experience of an Android tablet (especially for touchscreen interactions and on-screen keyboard support).\u003c/p\u003e","title":"Raspberry pi powered wall mounted tablet"},{"content":"After building out my cloud@home environment (detailed in my previous article), I decided to take the next logical step: replacing my OpenHABian setup with a fully GitOps-driven “as code” version of OpenHAB deployed into my K3s cluster.\nMy goals were simple:\nbe able to deploy or redeploy everything from scratch in seconds, support upgrades cleanly, optionally preserve user data when I want to, have transparency and reproducibility via Git. be able to reflect any OpenHab textual definition change in production with a simple commit This post walks through how I designed, configured, and now run OpenHAB on Kubernetes using GitOps—covering the trade-offs I faced, the architecture I settled on, and lessons learned along the way.\nOpenHab Deployment definition\nBefore diving in, you may want to review my k3s architecture— it provides the cluster with redundant persistence, automated backup and restore, and a fully GitOps-driven workflow powered by SaltStack and GitHub.\nWhat follows is a simplified and commented excerpt of the full YAML manifest. The goal is to highlight the core building blocks of the deployment, which serve as the foundation for everything else described in this article.\nPlease note that I\u0026rsquo;m not defining a persitent volume for Openhab \u0026ldquo;userdata\u0026rdquo; folder below because it is useless in the context of this article and would just add uneeded lines.\n# Define the Openhab web interface service. # It maps the default openhab port (8080) to the http standard 80 --- apiVersion: v1 kind: Service metadata: name: openhab-web-service namespace: prod spec: selector: app: openhab type: LoadBalancer ports: - protocol: TCP port: 80 # exposed port web admin targetPort: 8080 # targeting port on the container # Define the ingress route which uses the previous services # Note that the used domain is defined on my internal DNS server --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: openhab namespace: prod spec: rules: - host: openhab.local.lan http: paths: - path: / pathType: Prefix backend: service: name: openhab-web-service port: number: 80 # Persistent volume definition # I use it to persist data that Openhab need to store during runtime # It is also used to provides Openhab with textuals definitions (items, rules, etc.) --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: openhab-data-pv-claim namespace: prod labels: app: openhab spec: accessModes: - ReadWriteOnce resources: requests: storage: 3500M storageClassName: local-storage volumeName: openhab-data-pv --- apiVersion: v1 kind: PersistentVolume metadata: name: openhab-data-pv namespace: prod spec: capacity: storage: 3500M volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage claimRef: name: openhab-data-pv-claim namespace: prod local: path: /media/pv_openhab # Openhab container deployment definition --- apiVersion: apps/v1 kind: Deployment metadata: name: openhab-deployment namespace: prod spec: replicas: 1 # I don\u0026#39;t think Openhab support mutiple instances selector: matchLabels: app: openhab template: metadata: namespace: prod labels: app: openhab spec: containers: - name: openhab image: openhab/openhab:5.0.1 ports: - containerPort: 8080 name: http protocol: TCP env: - name: TZ value: Europe/Paris - name: OPENHAB_CONF value: /openhab/conf volumeMounts: - name: etc-localtime mountPath: /etc/localtime readOnly: true - name: openhab mountPath: /openhab/userdata subPath: userdata readOnly: false - name: openhab mountPath: /openhab/addons subPath: addons readOnly: false - name: openhab mountPath: /openhab/.java subPath: java readOnly: false - name: openhab mountPath: /openhab/.karaf subPath: karaf readOnly: false volumes: - name: etc-localtime hostPath: path: /usr/share/zoneinfo/Europe/Paris - name: openhab persistentVolumeClaim: claimName: openhab-data-pv-claim Some additional notes regarding this definition:\nI always specify the exact version of the Openhab container in order to ensure that only version I tested my configuration on will be deployed. I do not map the Karaf console in my production environement (8101) because using any TCP ingress in k3s requires a specific configuration and I honestly don\u0026rsquo;t need it. GitOPS for Openhab configuration\nTo manage OpenHAB configuration via GitOps, I use a git-sync sidecar container that pulls configuration from a private repository. The configuration is stored on an in-memory emptyDir volume mounted at /openhab/conf. Initially, I tried using a subpath mount, but this approach introduced two problems:\nStartup ordering – git-sync must complete its initial sync before the OpenHAB container starts, otherwise OpenHAB creates its own /conf directory structure, which may conflict. Continuous updates – subsequent changes in the Git repository are not reflected in the running container because symlinks in subpaths are not followed after startup. Using an initContainer for git-sync isn’t a solution either, because an initContainer only runs to completion once at startup, while the purpose of git-sync is to continuously monitor and update the local repository.\nThe solution I implemented involves three key steps:\nSet the OPENHAB_CONF environment variable to point to a different path, avoiding the need for subpath mounts. Use a sidecar container with git-sync to fetch the configuration from the OpenHAB repository. Use git-sync’s command hook feature to copy updated configuration files into the OpenHAB configuration directory. Two remaining challenges had to be addressed:\nRace condition at startup – if the OpenHAB container initializes its configuration directory before or during the git-sync copy, file structure may be incomplete (OpenHAB run some initialization command durint its first startup) Command hook limitations – git-sync only allows executing a single executable with no arguments. To solve this, I added a small BusyBox initContainer and a dedicated volume to create a bash script. This script acts as a command hook for git-sync, waiting for the OpenHAB configuration directory to exist before copying files over.\nFinally, there are a few practical notes:\nOnly older git-sync images (3.x) are readily available online. To ensure future compatibility, I built a custom git-sync image using a build script stored in my Git repository. I modified the GROUP_ID and USER_ID in the OpenHAB container definition so that the OpenHAB user and group match those used by git-sync, which cannot be changed otherwise. Here is the updated deployement Yaml code:\napiVersion: apps/v1 kind: Deployment metadata: name: openhab-deployment namespace: prod spec: replicas: selector: matchLabels: app: openhab template: metadata: namespace: prod labels: app: openhab spec: initContainers: - name: init-openhab-conf image: busybox command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;] args: - | cat \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; \u0026gt; /scripts/update.sh #!/bin/sh until [ -d /gitconfig/conf ] do sleep 5 done rm -Rf /gitconfig/conf/* cp -R /gitconfig/openhab-config/* /gitconfig/conf/ EOF chmod +x /scripts/update.sh # The same volume is also mounted on the gitsync container # so the update.sh script can be used as a command hook volumeMounts: - name: scripts mountPath: /scripts readOnly: false containers: - name: openhab image: openhab/openhab:5.0.1 ports: - containerPort: 8080 name: http protocol: TCP env: - name: TZ value: Europe/Paris - name: USER_ID # have to be the same as git-sync user value: \u0026#34;65533\u0026#34; - name: GROUP_ID value: \u0026#34;65533\u0026#34; # have to be the same as git-sync group # The conf dir must not be the same as the OpenHab default one - name: OPENHAB_CONF value: /openhab/conf/conf volumeMounts: - name: etc-localtime mountPath: /etc/localtime readOnly: true - name: gitconfig mountPath: /openhab/conf readOnly: false - name: openhab mountPath: /openhab/userdata subPath: userdata readOnly: false - name: openhab mountPath: /openhab/addons subPath: addons readOnly: false - name: openhab mountPath: /openhab/.java subPath: java readOnly: false - name: openhab mountPath: /openhab/.karaf subPath: karaf readOnly: false # The git-sync image is already imported in my k3s registry - name: git-sync image: gcr.io/k8s-staging-git-sync/git-sync:v4.2.2__linux_amd64 env: - name: GITSYNC_PASSWORD valueFrom: secretKeyRef: name: kubesecrets key: gitpwd args: - \u0026#34;--repo=https://github.com/jit06/openhab-config.git\u0026#34; - \u0026#34;--depth=1\u0026#34; - \u0026#34;--period=10s\u0026#34; - \u0026#34;--root=/gitconfig\u0026#34; - \u0026#34;--username=jit06\u0026#34; - \u0026#34;--ref=main\u0026#34; - \u0026#34;--link=openhab-config\u0026#34; - \u0026#34;--exechook-command=/scripts/update.sh\u0026#34; volumeMounts: - name: gitconfig mountPath: /gitconfig readOnly: false # remember: the same volume in which update.sh has been created - name: scripts mountPath: /scripts readOnly: true volumes: - name: etc-localtime hostPath: path: /usr/share/zoneinfo/Europe/Paris - name: openhab persistentVolumeClaim: claimName: openhab-data-pv-claim - name: gitconfig emptyDir: sizeLimit: 5Mi medium: Memory - name: scripts emptyDir: sizeLimit: 1Mi medium: Memory Secrets management So far, I’ve described how I set up a GitOps-driven OpenHAB: all textual definitions— items, rules, sitemaps, etc.—can be updated in my K3s production environment with a simple commit to the main branch.\nFor small changes, such as adjusting a cron value in a rule, I can edit the required file and commit it directly. For more complex changes, I create a dedicated branch in my development environment and merge it into the main branch once the work is complete.\nBut the goal isn’t fully achieved yet: OpenHAB textual definitions often need to store secrets or contextual values, for example:\nMQTT client ID Passwords used by bindings like Kodi Credentials required by external services such as InfluxDB To handle this, I implemented a simple but effective templating mechanism: any value enclosed in double brackets is replaced by the corresponding environment variable when files are fetched from the Git repository. For instance, {{MY_ENV_VAR}} will be replaced with the value of the environment variable MY_ENV_VAR.\nThis approach allows me to define all secrets directly in K3s and “restore” them at deployment time using environment variable definitions in the deployment YAML.\nIn practice, my openhab-config Git repository contains a build.sh script. This script is executed by the dynamically created updated.sh script described in the previous chapter. It relies solely on simple shell commands available in BusyBox, keeping the process lightweight and fully reproducible.\nHere is the build.sh script:\n#!/bin/bash #================================================================== # # This script is a lightweigth templating system. It replaces # strings like {{my_value}} by the corresponding environement # variable. # # This script is compatible with busybox: it uses simple shell # commands, \u0026#34;ls\u0026#34; and \u0026#34;sed\u0026#34; because \u0026#34;find\u0026#34; may not be available # # In order to be safer, only variables prefixed by \u0026#34;OPENHABTPL_\u0026#34; are # considered # #================================================================== for var in \u0026#34;${!OPENHABTPL_@}\u0026#34;; do escaped_value=$(printf \u0026#39;%s\\n\u0026#39; \u0026#34;${!var}\u0026#34; | sed -e \u0026#39;s/[\\/\u0026amp;]/\\\\\u0026amp;/g\u0026#39;) for i in $(ls -d \u0026#34;$(dirname \u0026#34;$(realpath $0)\u0026#34;)\u0026#34;/*/* ); do if [ ! -d $i ]; then sed -i \u0026#34;s/{{${var}}}/$escaped_value/g\u0026#34; $i fi done done The script build part in the BusyBox initcontainers call this build script:\ncat \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; \u0026gt; /scripts/update.sh #!/bin/sh until [ -d /gitconfig/conf ] do sleep 5 done rm -Rf /gitconfig/conf/* cp -R /gitconfig/openhab-config/* /gitconfig/conf/ chmod +x /gitconfig/conf/build.sh /gitconfig/conf/build.sh EOF chmod +x /scripts/update.sh Any secret or simple variable value can now be defined as environement variable in the git-sync sidecar container.\nFor example, below is my influxdb.cfg stored in the /services of my openhab-config repository\nversion=V2 url={{OPENHABTPL_INFLUXDB_HOST}} user=admin token={{OPENHABTPL_INFLUXDB_TOKEN}} db=bluemind retentionPolicy=default To better illustrate this, based on the same git-sync YAML definition as previously, but with additionnal values for InfluxDB:\n- name: git-sync image: gcr.io/k8s-staging-git-sync/git-sync:v4.2.2__linux_amd64 env: - name: OPENHABTPL_INFLUXDB_HOST value: \u0026#34;http://influxdb.local.lan\u0026#34; - name: OPENHABTPL_INFLUXDB_TOKEN valueFrom: secretKeyRef: name: kubesecrets key: influxdbtoken - name: GITSYNC_PASSWORD valueFrom: secretKeyRef: name: kubesecrets key: gitpwd args: - \u0026#34;--repo=https://github.com/jit06/openhab-config.git\u0026#34; - \u0026#34;--depth=1\u0026#34; - \u0026#34;--period=10s\u0026#34; - \u0026#34;--root=/gitconfig\u0026#34; - \u0026#34;--username=jit06\u0026#34; - \u0026#34;--ref=main\u0026#34; - \u0026#34;--link=openhab-config\u0026#34; - \u0026#34;--exechook-command=/scripts/update.sh\u0026#34; volumeMounts: - name: gitconfig mountPath: /gitconfig readOnly: false - name: scripts mountPath: /scripts readOnly: true Openhab Tips This section isn’t meant to replace the official OpenHAB design patterns, but rather to share a handful of practical tips that helped me improve and streamline my textual configuration. These are lessons learned while running OpenHAB being or not in a GitOps-driven setup, and they might be useful if you’re looking to optimize your own configuration.\nGeneral Practices One practice I strongly recommend is keeping the semantic model in a dedicated items file. This way, the logical organization of the home (rooms, zones, equipment) remains clearly separated from the technical items that implement automations.\nHere is an example of how I structure the semantic model:\nGroup lGlobal \u0026#34;Maison\u0026#34; \u0026lt;house\u0026gt; [\u0026#34;House\u0026#34;] Group lInterieur \u0026#34;Intérieur\u0026#34; \u0026lt;corridor\u0026gt; [\u0026#34;Indoor\u0026#34;] Group lRez \u0026#34;Rez\u0026#34; \u0026lt;groundfloor\u0026gt; (lInterieur) [\u0026#34;GroundFloor\u0026#34;] Group lEntree \u0026#34;Entrée\u0026#34; \u0026lt;corridor\u0026gt; (lRez) [\u0026#34;Entry\u0026#34;] Group lSde \u0026#34;Salle d\u0026#39;eau\u0026#34; \u0026lt;bath\u0026gt; (lRez) [\u0026#34;Bathroom\u0026#34;] Group lCuisine \u0026#34;Cuisine\u0026#34; \u0026lt;Kitchen\u0026gt; (lRez) [\u0026#34;Kitchen\u0026#34;] Group lSejour \u0026#34;Séjour\u0026#34; \u0026lt;party\u0026gt; (lRez) [\u0026#34;DiningRoom\u0026#34;] Group lSalon \u0026#34;Salon\u0026#34; \u0026lt;sofa\u0026gt; (lRez) [\u0026#34;LivingRoom\u0026#34;] Group lGarage \u0026#34;Garage\u0026#34; \u0026lt;cellar\u0026gt; (lRez) [\u0026#34;LaundryRoom\u0026#34;] Group lMusique \u0026#34;Musique\u0026#34; \u0026lt;office\u0026gt; (lRez) [\u0026#34;Office\u0026#34;] Group lEtage \u0026#34;Etage\u0026#34; \u0026lt;firstfloor\u0026gt; (lInterieur) [\u0026#34;FirstFloor\u0026#34;] Group lSdj \u0026#34;Salle de jeu\u0026#34; \u0026lt;projector\u0026gt; (lEtage) [\u0026#34;Room\u0026#34;] Group lSdb \u0026#34;Salle de bain\u0026#34; \u0026lt;bath\u0026gt; (lEtage) [\u0026#34;Bathroom\u0026#34;] Group lChMaxou \u0026#34;Chambre Maxou\u0026#34; \u0026lt;bedroom\u0026gt; (lEtage) [\u0026#34;Bedroom\u0026#34;] Group lChLoulou \u0026#34;Chambre Loulou\u0026#34; \u0026lt;bedroom\u0026gt; (lEtage) [\u0026#34;Bedroom\u0026#34;] Group lChParents \u0026#34;Chambre Parents\u0026#34; \u0026lt;bedroom\u0026gt; (lEtage) [\u0026#34;Bedroom\u0026#34;] Group lExterieur \u0026#34;Extérieur\u0026#34; \u0026lt;garden\u0026gt; [\u0026#34;Outdoor\u0026#34;] Group lParking \u0026#34;Parking\u0026#34; \u0026lt;garage\u0026gt; (lExterieur) [\u0026#34;Garage\u0026#34;] Group lCote \u0026#34;Cote\u0026#34; \u0026lt;garden\u0026gt; (lExterieur) [\u0026#34;Garden\u0026#34;] Group lJardin \u0026#34;Jardin\u0026#34; \u0026lt;lawnmower\u0026gt; (lExterieur) [\u0026#34;Garden\u0026#34;] Group lTerasse \u0026#34;Terasse\u0026#34; \u0026lt;terrace\u0026gt; (lExterieur) [\u0026#34;Terrace\u0026#34;] Another useful approach is to split items into files by “family”—for example, cameras.items, heaters.items, or sensors.items. Since items of the same family usually share a common structure, maintaining them in separate files makes updates and refactoring much easier.\nI also define a dedicated group for each item family. This allows me to manage or reference all items of a given type through their group, which comes in handy for both rules and UI navigation.\nIn addition, I create functional groups for rule logic. For example, I maintain groups for all battery-powered devices, for disabling all cameras, or for starting heaters. This simplifies rule writing because I can iterate over these groups instead of managing each item individually.\nHere is an example of how I assign items to their family group:\nGroup gHeater \u0026#34;Chauffages\u0026#34; \u0026lt;radiator\u0026gt; Group gHeaterSwitch \u0026#34;Chauffages - Contrôles\u0026#34; \u0026lt;switch\u0026gt; Group gHeaterPower \u0026#34;Chauffages - Conso instantannées\u0026#34; \u0026lt;energy\u0026gt; Group gHeaterDayPower \u0026#34;chauffages - Conso journalière\u0026#34; \u0026lt;energy\u0026gt; Group gHeaterTempEco \u0026#34;chauffages - Température ECO\u0026#34; \u0026lt;temperature\u0026gt; Group gHeaterTempConf \u0026#34;chauffages - Température CONF\u0026#34; \u0026lt;temperature\u0026gt; Group gHeaterTempTarg \u0026#34;chauffages - Température Cible\u0026#34; \u0026lt;temperature\u0026gt; Group:Switch gHeaterConf \u0026#34;chauffages - CONF / ECO\u0026#34; \u0026lt;switch\u0026gt; Group:Switch gHeaterAuto \u0026#34;chauffages - Gestion Auto\u0026#34; \u0026lt;switch\u0026gt; Group Heater_Garage \u0026#34;Chauffage Garage\u0026#34; \u0026lt;radiator\u0026gt; (lGarage,gHeater) [\u0026#34;HVAC\u0026#34;] Switch Heater_Garage_State \u0026#34;Contrôle\u0026#34; \u0026lt;switch\u0026gt; (Heater_Garage,gHeaterSwitch) [\u0026#34;RadiatorControl\u0026#34;] { channel=\u0026#34;mqtt:topic:smartplug2:state\u0026#34; } Number Heater_Garage_Power \u0026#34;Conso actuelle [%.3f Wh]\u0026#34; \u0026lt;energy\u0026gt; (Heater_Garage,gHeaterPower) [\u0026#34;Measurement\u0026#34;] { channel=\u0026#34;mqtt:topic:smartplug2:power\u0026#34; } Number Heater_Garage_DayPower \u0026#34;Conso Jour [%.3f Wh]\u0026#34; \u0026lt;energy\u0026gt; (Heater_Garage,gHeaterDayPower) [\u0026#34;Measurement\u0026#34;] { channel=\u0026#34;mqtt:topic:smartplug2:today\u0026#34; } Number Heater_Garage_TempEco \u0026#34;Température ECO [%.1f °C]\u0026#34; \u0026lt;temperature\u0026gt; (Heater_Garage,gHeaterTempEco) [\u0026#34;Control\u0026#34;] {widget=\u0026#34;oh-stepper\u0026#34;[step=\u0026#34;0.5\u0026#34;,min=\u0026#34;10\u0026#34;,max=\u0026#34;25\u0026#34;,enableInput=\u0026#34;true\u0026#34;,autorepeat=\u0026#34;true\u0026#34;]} Number Heater_Garage_TempConf \u0026#34;Température CONF [%.1f °C]\u0026#34; \u0026lt;temperature\u0026gt; (Heater_Garage,gHeaterTempConf) [\u0026#34;Control\u0026#34;] {widget=\u0026#34;oh-stepper\u0026#34;[step=\u0026#34;0.5\u0026#34;,min=\u0026#34;10\u0026#34;,max=\u0026#34;25\u0026#34;,enableInput=\u0026#34;true\u0026#34;,autorepeat=\u0026#34;true\u0026#34;]} Number Heater_Garage_TempTarg \u0026#34;Température Cible [%.1f °C]\u0026#34; \u0026lt;temperature\u0026gt; (Heater_Garage,gHeaterTempTarg) [\u0026#34;Control\u0026#34;] {widget=\u0026#34;oh-stepper\u0026#34;[step=\u0026#34;0.5\u0026#34;,min=\u0026#34;10\u0026#34;,max=\u0026#34;25\u0026#34;,enableInput=\u0026#34;true\u0026#34;,autorepeat=\u0026#34;true\u0026#34;]} Switch Heater_Garage_Conf \u0026#34;Confort\u0026#34; \u0026lt;switch\u0026gt; (Heater_Garage,gHeaterConf) [\u0026#34;RadiatorControl\u0026#34;] Switch Heater_Garage_Auto \u0026#34;Gestion auto\u0026#34; \u0026lt;switch\u0026gt; (Heater_Garage,gHeaterAuto) [\u0026#34;RadiatorControl\u0026#34;] Group Heater_Musique \u0026#34;Chauffage Musique\u0026#34; \u0026lt;radiator\u0026gt; (lMusique,gHeater) [\u0026#34;HVAC\u0026#34;] Switch Heater_Musique_State \u0026#34;Contrôle\u0026#34; \u0026lt;switch\u0026gt; (Heater_Musique,gHeaterSwitch) [\u0026#34;RadiatorControl\u0026#34;] { channel=\u0026#34;mqtt:topic:smartplug9:state\u0026#34; } Number Heater_Musique_Power \u0026#34;Conso actuelle [%.3f Wh]\u0026#34; \u0026lt;energy\u0026gt; (Heater_Musique,gHeaterPower) [\u0026#34;Measurement\u0026#34;] { channel=\u0026#34;mqtt:topic:smartplug9:power\u0026#34; } Number Heater_Musique_DayPower \u0026#34;Conso Jour [%.3f Wh]\u0026#34; \u0026lt;energy\u0026gt; (Heater_Musique,gHeaterDayPower) [\u0026#34;Measurement\u0026#34;] { channel=\u0026#34;mqtt:topic:smartplug9:today\u0026#34; } Number Heater_Musique_TempEco \u0026#34;Température ECO [%.1f °C]\u0026#34; \u0026lt;temperature\u0026gt; (Heater_Musique,gHeaterTempEco) [\u0026#34;Control\u0026#34;] {widget=\u0026#34;oh-stepper\u0026#34;[step=\u0026#34;0.5\u0026#34;,min=\u0026#34;10\u0026#34;,max=\u0026#34;25\u0026#34;,enableInput=\u0026#34;true\u0026#34;,autorepeat=\u0026#34;true\u0026#34;]} Number Heater_Musique_TempConf \u0026#34;Température CONF [%.1f °C]\u0026#34; \u0026lt;temperature\u0026gt; (Heater_Musique,gHeaterTempConf) [\u0026#34;Control\u0026#34;] {widget=\u0026#34;oh-stepper\u0026#34;[step=\u0026#34;0.5\u0026#34;,min=\u0026#34;10\u0026#34;,max=\u0026#34;25\u0026#34;,enableInput=\u0026#34;true\u0026#34;,autorepeat=\u0026#34;true\u0026#34;]} Number Heater_Musique_TempTarg \u0026#34;Température Cible [%.1f °C]\u0026#34; \u0026lt;temperature\u0026gt; (Heater_Musique,gHeaterTempTarg) [\u0026#34;Control\u0026#34;] {widget=\u0026#34;oh-stepper\u0026#34;[step=\u0026#34;0.5\u0026#34;,min=\u0026#34;10\u0026#34;,max=\u0026#34;25\u0026#34;,enableInput=\u0026#34;true\u0026#34;,autorepeat=\u0026#34;true\u0026#34;]} Switch Heater_Musique_Conf \u0026#34;Confort\u0026#34; \u0026lt;switch\u0026gt; (Heater_Musique,gHeaterConf) [\u0026#34;RadiatorControl\u0026#34;] Switch Heater_Musique_Auto \u0026#34;Gestion auto\u0026#34; \u0026lt;switch\u0026gt; (Heater_Musique,gHeaterAuto) [\u0026#34;RadiatorControl\u0026#34;] For rules, I keep a dedicated file for notifications. This makes all notification logic centralized and easy to extend without digging into unrelated automation files (more on that later).\nFinally, I maintain a dedicated file for startup actions. At startup, this file sets default values for items not linked to any binding—for instance, my comfort and eco temperature targets, or the heater hysteresis values.\nHere’s a snippet showing how I initialize default values at startup:\n// *************************************************** // SETTINGS // *************************************************** val DEFAULT_TEMP_ECO=\u0026#34;17.5\u0026#34; val DEFAULT_TEMP_CONF=\u0026#34;20.5\u0026#34; // *************************************************** // FUNCTIONS // *************************************************** val initItems = [GroupItem itemsToInit, String stateValue | itemsToInit.members.forEach[ GenericItem item | if(item.state == NULL || item.state == UNDEF) { item.postUpdate(stateValue) } ] ] // *************************************************** // RULE // *************************************************** rule \u0026#34;Initialisation des valeurs par défaut\u0026#34; when System started then initItems.apply(gHeaterTempEco\t, DEFAULT_TEMP_ECO) initItems.apply(gHeaterTempConf\t, DEFAULT_TEMP_CONF) initItems.apply(gHeaterTempTarg\t, DEFAULT_TEMP_ECO) initItems.apply(gHeaterAuto\t, \u0026#34;OFF\u0026#34;) end HTTP Binding Usage despite the doc give hints about a few types like \u0026ldquo;switch\u0026rdquo;, it seems to support other types too. At least, I successfully used Numbers\nHere’s a snippet showing how to get CPU usage from some API:\nThing http:url:mything \u0026#34;mything name\u0026#34;[ baseURL=\u0026#34;http://mything.local.lan/cgi-bin/api.cg\u0026#34;, contentType=\u0026#34;application/json\u0026#34; ,refresh=5, stateMethod=\u0026#34;GET\u0026#34;] { Channels: Type number : cpu_used \u0026#34;CPU\u0026#34; [ mode=\u0026#34;READONLY\u0026#34;, stateExtension=\u0026#34;\u0026amp;cmd=GetPerformance\u0026#34;, stateTransformation=\u0026#34;JSONPATH($[0].value.Performance.cpuUsed)\u0026#34; ] } Also commandTransformation and stateTransformation are not only about transformation. You can eventualy return any value you need, providing it is always a string ! So returning a JSON structure as POST payload must be \u0026ldquo;stringified\u0026rdquo; before (see how the Reolink camera control chapter below).\nAs an exemple, here is a javascript transformation that enable or disable email alert on a Reolink camera:\nvar obj = [{ \u0026#34;cmd\u0026#34;: \u0026#34;SetEmail\u0026#34;, \u0026#34;action\u0026#34;: 0, \u0026#34;param\u0026#34;: { \u0026#34;Email\u0026#34;: { \u0026#34;schedule\u0026#34;: { \u0026#34;enable\u0026#34;: input === \u0026#34;1\u0026#34; ? 1 : 0 } } } }]; JSON.stringify(obj); Also note that this kiong of transormation needs the jsscripting automation in addons.cfg\nIntegrating Moodaudio Nothing groundbreaking here, but it’s worth mentioning: Moodaudio doesn’t provide a dedicated binding like Kodi does. However, it can still be controlled effectively using the MPD binding, which supports standard commands such as play, pause, previous, and next.\nHere’s a simple example of how I use the MPD binding with Moodaudio:\nThing mpd:mpd:moodaudio-salon \u0026#34;MoodAudio Salon\u0026#34; @ \u0026#34;LivingRoom\u0026#34;[ipAddress=\u0026#34;moodaudio.local.lan\u0026#34;, port=6600] In addition, I implemented a “ one button radio play” feature by calling the Moodaudio API directly. The trick is to use the /command/cmd? endpoint with the playitem command, followed by the RADIO/\u0026lt;name of the radio\u0026gt; argument.\nHere’s how the command looks in practice:\nThing http:url:moodaudio-salon \u0026#34;MoodAudio Salon\u0026#34; @ \u0026#34;LivingRoom\u0026#34;[ baseURL=\u0026#34;http://moodaudio.local.lan\u0026#34;, commandMethod=\u0026#34;GET\u0026#34;, contentType=\u0026#34;text/plain\u0026#34; ] { Channels: Type switch : play_radio_nostalgie \u0026#34;Clear / play item\u0026#34; [ mode=\u0026#34;WRITEONLY\u0026#34;, commandExtension=\u0026#34;/command/?cmd=%2$s\u0026#34;, onValue=\u0026#34;playitem RADIO%%2FNostalgie.pls\u0026#34;, offValue=\u0026#34;clear\u0026#34; ] } If you don\u0026rsquo;t have a proper TLS certificate, you will need to add ignoreSSLErrors=\u0026quot;true\u0026quot; option on the thing definition\nPush Notifications Just like with my Zabbix monitoring setup (described in my cloud@home article), I rely on Ntfy for push notifications to provide a simple yet effective way of sending alerts directly from rules. This keeps me informed about important events without having to constantly check dashboards.\nTo keep things tidy, I group all my notification logic into a single rules file. This centralization makes it easier to maintain, extend, or troubleshoot the notification system as my setup evolves.\nHere’s how my notification rules are defined (sample):\n// post a plain text message to NTFY url // - String Message: message to send val sendNotification = [ String message | sendHttpPostRequest(\u0026#34;http://ntfy.sh/mycustomchannel\u0026#34;, \u0026#34;text/plain\u0026#34;, message) ] rule \u0026#34;Notification - (re)Démarrage d\u0026#39;OpenHab\u0026#34; when System started then sendNotification.apply(\u0026#34;Openhab a (re)démarré\u0026#34;) end ////////////// Batteries /////////////// rule \u0026#34;Notification - Sonde - batterie faible\u0026#34; when Member of gSensorBatt changed to \u0026#34;LOW\u0026#34; then sendNotification.apply(\u0026#34;Piles à changer sonde \u0026#39;\u0026#34; + triggeringItem.name +\u0026#34;\u0026#39;\u0026#34;) end Monitoring Oregon Sensor Reception Depending on battery level, signal quality, or sometimes for no obvious reason, my receiver may occasionally fail to capture readings from Oregon temperature sensors. To detect and act on these situations, I implemented a simple notification mechanism.\nFor each sensor, I define a dedicated DateTime item that uses the system:timestamp-update profile. All these items are then grouped together, so I can easily process them in bulk.\nHere’s an example of how I declare the items and group:\nGroup Sensor_Garage \u0026#34;Sonde Garage\u0026#34; \u0026lt;temperature\u0026gt; (lGarage,gSensor) [\u0026#34;Sensor\u0026#34;] Number:Temperature Sensor_Garage_Temp \u0026#34;Temperature [%.1f °C]\u0026#34; \u0026lt;temperature\u0026gt; (Sensor_Garage,gSensorTemp) [\u0026#34;Temperature\u0026#34;] { channel=\u0026#34;mqtt:topic:mqtt-garage:sensor_garage_temp\u0026#34; } String Sensor_Garage_Batt \u0026#34;Batterie\u0026#34; \u0026lt;batterylevel\u0026gt; (Sensor_Garage,gSensorBatt) [\u0026#34;LowBattery\u0026#34;] { channel=\u0026#34;mqtt:topic:mqtt-garage:sensor_garage_batt\u0026#34; } DateTime Sensor_Garage_Updt \u0026#34;MàJ [%1$ta %1$tR]\u0026#34; \u0026lt;time\u0026gt; (Sensor_Garage,gSensorUpdt) [\u0026#34;Timestamp\u0026#34;] { channel=\u0026#34;mqtt:topic:mqtt-garage:sensor_garage_temp\u0026#34;[profile=\u0026#34;system:timestamp-update\u0026#34;] } On top of this, I created a notification rule that runs every 10 minutes. The rule checks whether any group member has an outdated timestamp and triggers a notification if a sensor hasn’t reported for too long.\nHere’s how the monitoring rule is defined:\nval SENSORS_SIGNAL_TIMEOUT = 15 rule \u0026#34;Notification - Sondes Oregon - reception\u0026#34; when Time cron \u0026#34;0 0/10 * * * ? *\u0026#34; then gSensorUpdt.members.forEach[ GenericItem item | if(item.state != NULL \u0026amp;\u0026amp; item.state != UNDEF ) { if(now.minusMinutes(SENSORS_SIGNAL_TIMEOUT).isAfter((item.state as DateTimeType).getZonedDateTime(ZoneId.systemDefault))) { sendNotification.apply(\u0026#34;Pas de signal de la sonde \u0026#34; + item.name +\u0026#34; depuis plus de \u0026#34;+SENSORS_SIGNAL_TIMEOUT+\u0026#34; minutes\u0026#34;) } } ] end These tips don’t aim to replace the official OpenHAB design patterns, but they highlight a few practical tricks that helped me make my textual configuration cleaner, more maintainable, and easier to extend. Hopefully, they can serve as inspiration if you’re building or refining your own setup.\nLast words With this GitOps-driven approach, my OpenHAB deployment is now fully reproducible, portable, and easy to maintain. From the initial deployment definitions, to configuration management through Git, to handling secrets and notifications, everything is described “as code” and can be rolled out—or rolled back—in just a few seconds.\nWhat started as a simple replacement for my OpenHABian setup has turned into a robust, Kubernetes-native installation where upgrades, recovery, and experimentation come with almost no operational overhead.\nIf you’re already running a K3s cluster, this workflow shows how home automation can benefit from the same best practices as modern cloud-native applications: version control, GitOps pipelines, and declarative infrastructure.\n","permalink":"https://www.bluemind.org/openhab-code-k3s/","summary":"\u003cp\u003eAfter building out my cloud@home environment (detailed in my \u003ca href=\"/cloud-home-pxe-proxmox-saltstack-k3s-minimum-toil/\"\u003eprevious article\u003c/a\u003e), I decided to take the next logical step: replacing my \u003ca href=\"https://www.openhab.org/docs/installation/openhabian.html\"\u003eOpenHABian\u003c/a\u003e setup with a fully GitOps-driven “as code” version of \u003ca href=\"https://www.openhab.org/\"\u003eOpenHAB\u003c/a\u003e deployed into my \u003ca href=\"https://k3s.io/\"\u003eK3s\u003c/a\u003e cluster.\u003c/p\u003e\n\u003cp\u003eMy goals were simple:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ebe able to deploy or redeploy everything from scratch in seconds,\u003c/li\u003e\n\u003cli\u003esupport upgrades cleanly,\u003c/li\u003e\n\u003cli\u003eoptionally preserve user data when I want to,\u003c/li\u003e\n\u003cli\u003ehave transparency and reproducibility via Git.\u003c/li\u003e\n\u003cli\u003ebe able to reflect any OpenHab textual definition change in production with a simple commit\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis post walks through how I designed, configured, and now run OpenHAB on Kubernetes using GitOps—covering the trade-offs I faced, the architecture I settled on, and lessons learned along the way.\u003c/p\u003e","title":"GitOps driven Openhab in k3s"},{"content":"My home lab was 7 years old and it was time to replace it. It was based on five Odroid HC1 nodes and 1 Odroid N1 (which never reached the mass production stage, Hardkernel sent it to me as a gift for a debug party). 4 HC1 nodes were used for a Docker Swarm cluster and 1 was dedicated to Nginx as a reverse proxy / WAF / SSL offloader. Regarding the Odroid N1, it was used as a NAS and also as a saltstack master.\nEverything has been replaced with a major upgrade: faster infrastructure, better power efficiency, GitOps approach, virtualized environments, Kubernetes cluster, improved Saltstack implementation and last but not least: everything can be built automaticaly from scratch using PXE and the help of custom scripts !\nThis is the (very long) story of my new homelab build\u0026hellip;\nConcept: the big picture Before diving into all the details, I think it is important to understand what was goind through my mind, the concept I tried to implement (and hopefully succeeded in doing so).\nI\u0026rsquo;m not saying that it is the best approach. After all, it\u0026rsquo;s a homelab,which, to me, means a way to learn, have fun, express creativity, and, of course, self-host some services.\nSimplified GitOps approach I did not use any CI/CD components, no branches (at least for now), but the bare minimum for Infrastructure as Code is in place:\n100% \u0026ldquo;as code\u0026rdquo; infrastructure Git repository IaC frontend through Saltstack IaC backend implementation with Proxmox VE Infrastructure overview A homelab comes with certain constraints that sometimes prevent you from making the best technical choices. For me, the main concerns were:\nMoney: Obviously, resources are limited and must be optimized based on what you want or need to achieve. Space: Hardware takes up room, can be noisy, etc. Power consumption: For hardware running 24/7, this can be a real game-changer for the annual electricity bill. Ease of replacement: How easily things can be replaced in case of failure, as I don’t have tons of free time! Hardware In this homelab revision, I chose x86 hardware over ARM. The Intel N100 is a powerhouse in terms of performance, efficiency, and price. Not to mention that it offers far greater expansion options than ARM SBCs, especially when it comes to RAM and virtualization support.\nDevices Basically, my \u0026ldquo;cloud@home\u0026rdquo; setup consists of two Intel N100 nodes and one N6000 (Liva Z3). Since I have a small, wall-mounted 19\u0026quot; rack with a depth of 30 cm, I decided to build custom servers using rack enclosures.\nBelow is the list of components I used:\nItem Quantity Comments Intertech 1.5U-1528L 1 Case for Node-1. A surprisingly good enclosure for the price: only 1.5U in height, with two externally accessible 3.5″ bays for SATA drives. It supports a Mini-ITX motherboard and has a depth of 27.8 cm. Intertech IPC 1HU-K-125L 1 Case for Node-2. Similar to the previous one but with a 1U height and no externally accessible 3.5″ bay. Fortron FSP250-50FEB 2 A 250W Flex ATX power supply with up to 85% efficiency—useful for a setup running 24/7. ASUS Prime N100I-D D4-CSM 2 Mini-ITX motherboard with an integrated N100 processor. I found it to have a good balance of features and price. 32Gb Kingston FURY Impact, DDR4 3200 SO-DIMM 2 Contrary to the official specs, the N100 is not limited to 16GB of RAM—it works perfectly with 32GB. Kingston NV2 – 1Tb 2 Used for virtual machines and container storage. Offers good performance and capacity for the price. More than enough for my home lab. SATA SSD kingston kc600 256Gb 2 Boot disks and /root storage for Proxmox on both Node-1 and Node-2. Uni USB 3.0 to 2.5Gbps ethernet adatpter. 4 The ASUS motherboard has only a single gigabit NIC, which was too limiting for my needs. Adding two additional ports allows for bonding (LACP) plus a dedicated 2.5G node-to-node connection for the replication network and Corosync. I initially tried dual-port PCIe cards, but none worked properly (r8123, r8125, Intel 82571, and Intel i225-V). JMB582 based SATA 3 M.2 controller with 2 ports (Key A + E) 1 Used to connect the two hard drives for the NAS VM. I could have used a PCIe card, but the only available slot was initially occupied by a network card… well, initially. Seagate HDD 8Tb Sata 3 2 Used with the JMB582 controller for NAS storage. Liva Z3 – 128 Gb 1 Used as Node-3 for Proxmox quorum, Salt Master, Zabbix monitoring, and a third Kubernetes node (mainly to improve etcd cluster stability). Kingston fury DD4 3200 8Gb 2 16GB dual-channel RAM for the Liva Z3. Patriot P400 Lite 250 Gb 1 Low-power PCIe M.2 SSD for the Liva Z3 Bios settings I did some BIOS \u0026ldquo;tweaking\u0026rdquo; mostly to save power and to allow for PXE boot on all nodes (including wake-on-lan activation)\nAsus Prime N100I-D Here are the settings for the two N100 nodes:\ndvmt graphic memory set from 64 to 32 : no video memory needed for a headless server enabled sr-iov support for better offloading on NIC disabled all usb port but usb_3, usb_4, u32g1_e3 and u32g1_e4 which are the one mounted on the face and the one used for the etherner adapters. enabled network stack, ipv4 PXE and set PXE boot as first priority (can be done only after a reboot) set restore AC power loss to last state in case of power failure, so it will be powered back on enabed wake-on-lan (which is called \u0026ldquo;power on by pci-e\u0026rdquo;) so I could turn on my servers from pfsense easily disabled hdaudio, not needed, save power disabled wifi and bluetooth. Through, there are no such functionnalities installed disabled serial and parallel ports, save power enabled XMP as the memory I use support it. It might set timings to better ones, can\u0026rsquo;t hurt. Disable fast boot : server\u0026rsquo;s are not willing to reboot a lot and I prefer letting all hardware checks to be done when I reboot it enabled native aspm for power saving Liva z3 Power management / resume via PME : mandatory to allow wake-on-lan Disabled ACPI sleep state: I don\u0026rsquo;t need and don\u0026rsquo;t want it to be able to sleep as a 24/7 server Wireless function : disabled wifi and bluetooth, I do not need then and it can save power System agent configuration: set all memory values to mininum as it will be a headless machine, no need for GPU memory PCH configuration: disabled audio (not needed + power save), set restore AC power lost to power on Boot / network stack: enabled (wol), disabled quiet boot and set boot order to \u0026ldquo;usb, network, harddisk\u0026rdquo; disabled EUP else wake-on-lan won\u0026rsquo;t work Photos I took some photos during the \u0026ldquo;first\u0026rdquo; assembly process. There have been some change since I replaced the PCIe network cards by USB-3 adapters and finaly used 32Gb So-dimms.\nBase infrastructure Prepare for PXE boot As described previously, I configured all 3 nodes to boot via Network / PXE by default and fallback to internal storage. The main goal is to be able to \u0026ldquo;factory reset\u0026rdquo; any server while rebooting (all \u0026ldquo;post-install\u0026rdquo; configuration is then done by Saltstack).\nIf I want to restart one node from scratch, I just enable pxeboot on my PFsense box, set the corresponding boot file and reboot the device. Once the install has finished, I disable pxeboot and the device fallback to the media on which the fresh install has been done.\nIn Pfsense, I installed the package \u0026ldquo;tftpd\u0026rdquo;. then I configured it via the menu \u0026ldquo;Services / TFTPD Server\u0026rdquo;:\nEnable it Restrict to ip adresses of vlan with PXE enabled devices Set to ipv4 only as I don\u0026rsquo;t use IPv6 for now Open udp port 69 in the corresponding vlan Download IPXE and and add files on the TFTP Server (http://boot.ipxe.org/undionly.kpxe and http://boot.ipxe.org/ipxe.efi). In my case, I used scp to copy files on my PFsense box. Add an autoxec.ipxe file that will be launched by ipxe Bellow is the my autoxec.ipxe file:\n#!ipxe dhcp set webserver tftp://aaa.bbb.ccc.dddd initrd ${webserver}/pve-8.2/initrd || echo \u0026#34;error loading initrd\u0026#34; kernel ${webserver}/pve-8.2/linux26 initrd=initrd ramdisk_size=16777216 rw proxmox-start-auto-installer || echo \u0026#34;error loading kernel\u0026#34; boot Note that calling \u0026ldquo;dhcp\u0026rdquo; in first place seems to be redundant as the Bios / EFI already got an ip address to load ipxe. But In fact, without calling \u0026ldquo;dhcp\u0026rdquo;, I had timeout while loading the (quite big) Proxmox initrd file\u0026hellip;\nThen I configured PFsense\u0026rsquo;s DHCP service to send required information for PXE boot to work:\nUncheck ignore bootp queries set TFTP Server to the corresponding IP of the router (for each vlan on which tftpd should respond) Enable network booting Set the boot files : undionly.kpxe for BIOS and ipxe.efi for UEFI In order to be able to control and customize any node on my network, each one has a specific \u0026ldquo;autoexec.ipxe\u0026rdquo; and \u0026ldquo;initrd\u0026rdquo; files that I just rename when I need to re-stage them.\nI keep the default autoexec.ipxe with the \u0026ldquo;exit 1\u0026rdquo; command so by default, my servers boot to the next BIOS option. It is not mandatory but it makes the boot process faster when PXE is not enabled.\nNow booting any node from my network is possible and simply controled via my PFSense box.\nProxmox nodes provisioning PXE image creation What I wanted was to boot any node from the Proxmox ISO in \u0026ldquo;auto install\u0026rdquo; mode as explained on the wiki page https://pve.proxmox.com/wiki/Automated_Installation. This implies to generate a custom ISO image with my own answers file, then to convert the ISO into a PXE bootable file with https://github.com/morph027/pve-iso-2-pxe.\nI also needed to add some customizations to both install process and first boot.\nWhat I added to the the install process are:\nPatch Proxmox to allow installing on an emmc disk (for the Liva z3) Add a custom network interfaces file to setup the network correctly (vlan, bond, etc.) Add a custom rc.local file to further customize after the installation process During the first boot, I wanted to execute some actions so each node could be ready and fully configured with very few manual actions. This the purpose of the custom rc.local:\nConfigure network interfaces with the file injected in the ISO image Configure /dev/sda to be fully dedicated to proxmox root (other storages are on nvme) Remove enterprise repository and enable the community one Remove subscription nag uppon login Install saltstack so nodes can be automaticaly configured after the first boot Create a zfs pool on the nvme disk Enable wake-on-lan Create a first LXE container on node-3 to host the salt-master Create the Proxmox cluster on node-1 Each node is fully configured at first boot either by rc.local file or by saltstack which runs highstate for any new node.\nThis leads me to the create two Github projects:\nA public \u0026ldquo;generic\u0026rdquo; one that allows to generate PXE image with automated Proxmox install and custom files : https://github.com/jit06/pve-auto-pxe A private \u0026ldquo;specific\u0026rdquo; one: which contains my own custom files and a small build system to create all needed files in a folder that can be mounted as \u0026ldquo;/config\u0026rdquo; for pve-auto-pxe. Obviously, I cannot share the private repository as it contains some informations I don\u0026rsquo;t want to share, but basically this is what it does:\nCreate a build folder with hostname sub folders in it If a hostname\u0026rsquo;s specific rc.local exists, merge it with the main rc.local Generate a finalized rc.local and autoexec.pxe files for all hosts Ask for, then inject root password in answer.toml files Ask for, and inject saltstack and git passwords for the Liva z3 node This build script generates a kernel and initrd files that can be copied to the TFTP server on my Pfsense. The initrd file is 1.6Go in size and PFSense does not allow to copy such big file from the web interface. It has to be copied though ssh / scp.\nCustomized Proxmox images The Proxmox ISO image customization is here to set up things that are more or less hardware related or needed right after a clean install. Everything else is set and / or tuned with saltstack.\nAs explained earlier, the initial configuration is done via a custom rc.local script injected in the Proxmox ISO image. All nodes have the same rc.local base plus a specific one.\nBelow is the common rc.local. It is executed only once as it is replaced by a new one via saltstack as soon as the salt-minion is connected.\nClick to expand code#!/bin/sh -e # # rc.local # # This script is executed at the end of each multiuser runlevel. # any non 0 exit will be reported as failure by systemd # set new interfaces file if any if [ -f /etc/network/interfaces.install ]; then echo \u0026#34;found a network interfaces file to install\u0026#34; rm /etc/network/interfaces mv /etc/network/interfaces.install /etc/network/interfaces systemctl restart networking fi # /dev/sda is fully dedicated to pve root if [ -e /dev/pve/data ]; then echo \u0026#34;found /dev/pve/data LVM partition: remove it and extend /dev/pve/root\u0026#34; lvresize -l +100%FREE /dev/pve/root resize2fs /dev/mapper/pve-root sleep 10 fi # remove enterprise repo and enable the commnunity one\u0026#39;s if [ -f /etc/apt/sources.list.d/pve-enterprise.list ]; then echo \u0026#34;Enterprise repository found: disable it and enable commnunity repo\u0026#34; rm /etc/apt/sources.list.d/pve-enterprise.list rm /etc/apt/sources.list.d/ceph.list echo \u0026#34;deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription\u0026#34; \u0026gt;\u0026gt; /etc/apt/sources.list echo \u0026#34;Trigger update and full upgrade\u0026#34; apt update \u0026amp;\u0026amp; apt -y full-upgrade fi # remove the subscription NAG uppon login if [ ! -f /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.bak ]; then echo \u0026#34;subscription NAG found, removing it\u0026#34; sed -Ezi.bak \u0026#34;s/(Ext.Msg.show\\(\\{\\s+title: gettext\\(\u0026#39;No valid sub)/void\\(\\{ \\/\\/\\1/g\u0026#34; /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js systemctl restart pveproxy.service fi # install saltstack Onedir repo if [ ! -f /etc/apt/keyrings/salt-archive-keyring.pgp ]; then echo \u0026#34;Saltstack keyring not found, install it\u0026#34; wget --tries=3 --waitretry=3 --no-dns-cache --retry-on-host-error -O /etc/apt/keyrings/salt-archive-keyring.pgp https://packages.broadcom.com/artifactory/api/security/keypair/SaltProjectKey/public fi if [ ! -f /etc/apt/sources.list.d/salt.sources ]; then wget --tries=3 --waitretry=3 --no-dns-cache --retry-on-host-error -O /etc/apt/sources.list.d/salt.sources https://github.com/saltstack/salt-install-guide/releases/latest/download/salt.sources; apt update \u0026amp;\u0026amp; apt -y install salt-minion mkdir -p /etc/salt/minion.d echo \u0026#34;master: saltmaster.local.lan\u0026#34; \u0026gt;\u0026gt; /etc/salt/minion.d/master.conf echo \u0026#34;startup_states: highstate\u0026#34; \u0026gt;\u0026gt; /etc/salt/minion.d/minion.conf systemctl enable --now salt-minion fi # create zfs volumes on first nvme disk if not exists if [ $(zpool list | grep zfs-storage | awk \u0026#39;{print $1}\u0026#39;) = \u0026#34;zfs-storage\u0026#34; ]; then # do nothing echo \u0026#34;zfs-storage found\u0026#34; else echo \u0026#34;no zfs-storage pool found, creating it on /dev/nvme0n1\u0026#34; # erase partition table dd if=/dev/zero of=/dev/nvme0n1 bs=512 count=1 #create the pool zpool create -o autotrim=on -o ashift=12 zfs-storage /dev/nvme0n1 # disable access time and sync for better performnances zfs set atime=off zfs-storage zfs set sync=disabled zfs-storage # set compression zfs set compression=lz4 zfs-storage pvesm add zfspool storage -pool zfs-storage fi if [ -f /etc/default/grub.d/installer.cfg ]; then rm /etc/default/grub.d/installer.cfg update-grub fi # enable wake on lan on all physical interfaces nics=$(ip -pretty link show | \\grep enp | awk \u0026#39;{print $2}\u0026#39; | cut -d\u0026#39;:\u0026#39; -f 1) if [ -n \u0026#34;$nics\u0026#34; ]; then for interface in $nics do echo \u0026#34;Enable WOL for $interface\u0026#34; ethtool -s $interface wol g done fi As seen in the first lines, a dedicated /etc/network/interfaces.install is moved to replace the original one. This specific file is copied during the customization of the Proxmox ISO image. This file is very important because it set up the whole network. It is pretty similar on both node: a bridge dedicated to VM and LXC, and another bridge dedicated to the replication (corosync, zfs, etc.).\nThe interfaces file is like the following:\nClick to expand code########################################################## # # Physical interfaces : only used though virtual networks # ########################################################## auto lo iface lo inet loopback # USB 2.5G nic wired to the other node auto enx00e04c68030c iface enx00e04c68030c inet manual # internal 1GB nic bonded with USB 2.5GB auto enp2s0 iface enp2s0 inet manual # 2nd USB NIC bonded with internal NIC auto enx00e04c680b32 iface enx00e04c680b32 inet manual ########################################################## # # link aggregations # ########################################################## # main bond to host all vlans auto bond0 iface bond0 inet manual hwaddress xx:xx:xx:xx:xx:xx bond-slaves enp2s0 enx00e04c680b32 bond-miimon 100 bond-xmit-hash-policy layer2+3 bond-mode 802.3ad # LACP ########################################################## # # bridges # ########################################################## # bridge to host all vlan + management (untagged vlan 40) auto vmbr0 iface vmbr0 inet dhcp bridge-ports bond0 bridge-stp off bridge-fd 0 bridge-vlan-aware yes bridge-vids 2-4094 # vlan are handled on VM network card bridge_ageing 0 # Network for replication on dedicated 2.5G NIC auto vmbr1 iface vmbr1 inet static address 10.0.0.2/24 bridge-ports enx00e04c680b32 bridge-stp off bridge-fd 0 The node-1 has a special treatment because it has NL drives for NAS storage. At this stage, the script assumes that NAS drives are already formated (mkfs.ext4 -F -b 4096 /dev/sdx1) because in case of restaging, I dont want to risk any loss of data.\nThis first rc.local also creates the corosync cluster:\nClick to expand code#### create cluster if it does not exists if [ ! -f /etc/pve/corosync.conf ]; then pvecm create cluster --link0 address=aaa.bbb.ccc.ddd echo \u0026#34;migration: secure,network=10.0.0.0/24\u0026#34; \u0026gt;\u0026gt; /etc/pve/datacenter.cfg fi #### add qdevice for corosync quorum if [ ! -f /sbin/corosync-qdevice ]; then apt -y install corosync-qdevice fi #### setup storages if [ -z \u0026#34;$(grep \u0026#39;dir: vz\u0026#39; /etc/pve/storage.cfg)\u0026#34; ]; then # dedicated directory on zfs for iso and lxc templates zfs create zfs-storage/vz pvesm add dir vz --path /zfs-storage/vz --content \u0026#39;vztmpl,iso,snippets\u0026#39; # no content allowed on system disk pvesm set local --content \u0026#39;\u0026#39; # NL drives for nas and backup mkdir -p /nas-storage /nas-mirror echo \u0026#34;LABEL=NAS-STORAGE /nas-storage ext4 nofail,nodev,nosuid,relatime,noexec,async 0 2\u0026#34; \u0026gt;\u0026gt; /etc/fstab echo \u0026#34;LABEL=NAS-MIRROR /nas-mirror ext4 nofail,noauto,nodev,nosuid,relatime,noexec,async 0 2\u0026#34; \u0026gt;\u0026gt; /etc/fstab systemctl daemon-reload mount /nas-storage pvesm add dir backup --path /nas-storage/backup --content \u0026#39;backup\u0026#39; --nodes \u0026#39;node-1\u0026#39; fi Finally, the node-3 has a special treatment too on its rc.local: as this is the host for saltstack master, a dedicated LXC is created to be able to initialize everything else when all nodes are ready, including PGP initilization needed to encrypt and decrypt secrets that are stored on my private saltstack git repository (more details on this subject later).\nClick to expand code#### install corosync-qnetd package for external vote support on the cluster if [ ! -f /usr/bin/corosync-qnetd ]; then apt -y install corosync-qnetd fi if [ -z \u0026#34;$(grep \u0026#39;dir: vz\u0026#39; /etc/pve/storage.cfg)\u0026#34; ]; then # only template and iso allowed on system disk pvesm set local --content \u0026#39;vztmpl,iso\u0026#39; # zfs-storage host vm and lxc contents pvesm set storage --content \u0026#39;images,rootdir\u0026#39; fi #### create saltstack lxc if not exists if [ \u0026#34;$(pct list | grep saltmaster | awk \u0026#39;{print $1}\u0026#39;)\u0026#34; -eq 100 ]; then echo \u0026#34;found saltmaster lxc\u0026#34; else pveam update # download the needed template TEMPLATE=$(pveam available --section system | grep debian-12 | awk \u0026#39;{print $2}\u0026#39;) pveam download local $TEMPLATE # create the container pct create 100 /var/lib/vz/template/cache/$TEMPLATE \\ --cores 2 \\ --memory 2048 \\ --storage storage \\ --description \u0026#34;saltstack master for home cloud\u0026#34; \\ --hostname \u0026#34;saltmaster\u0026#34; \\ --onboot 1 \\ --ostype debian \\ --password \u0026#34;SALTMASTER_ROOT_PWD\u0026#34; \\ --swap 0 \\ --features nesting=1 \\ --net0 name=eth0,bridge=vmbr0,ip=dhcp,tag=xx sleep 2 pct start 100 # prepare and upgrade OS pct exec 100 -- bash -c \u0026#34;apt update \u0026amp;\u0026amp; apt -y upgrade \u0026amp;\u0026amp; apt -y install git gpg sudo\u0026#34; # install saltstack onedir repo pct exec 100 -- bash -c \u0026#39;\\ wget --tries=3 --waitretry=3 --no-dns-cache --retry-on-host-error -O /etc/apt/keyrings/salt-archive-keyring-2023.gpg https://repo.saltproject.io/salt/py3/debian/12/amd64/SALT-PROJECT-GPG-PUBKEY-2023.gpg echo \u0026#34;deb [signed-by=/etc/apt/keyrings/salt-archive-keyring-2023.gpg arch=amd64] https://repo.saltproject.io/salt/py3/debian/12/amd64/latest bookworm main\u0026#34; | tee /etc/apt/sources.list.d/salt.list\u0026#39; # install needed packages pct exec 100 -- bash -c \u0026#34;apt update \u0026amp;\u0026amp; apt -y install salt-master salt-minion\u0026#34; pct exec 100 -- bash -c \u0026#34;salt-pip install pyinotify croniter IPy\u0026#34; # clone the saltstack IAC git repo pct exec 100 -- bash -c \u0026#34;\\ rm -Rf /srv/* git clone https://XXX:SALTMASTER_GIT_PWD/XXX/saltstack /srv\u0026#34; # configure saltmaster pct exec 100 -- bash -c \u0026#39;\\ mkdir -p /etc/salt/master.d echo \u0026#34;cli_summary: True\u0026#34; \u0026gt;\u0026gt; /etc/salt/master.d/saltmaster.conf echo \u0026#34;auto_accept: True\u0026#34; \u0026gt;\u0026gt; /etc/salt/master.d/saltmaster.conf echo \u0026#34;state_output: changes\u0026#34; \u0026gt;\u0026gt; /etc/salt/master.d/saltmaster.conf systemctl enable --now salt-master\u0026#39; sleep 5 # configure saltminion pct exec 100 -- bash -c \u0026#39;\\ mkdir -p /etc/salt/minion.d echo \u0026#34;master: saltmaster.local.lan\u0026#34; \u0026gt;\u0026gt; /etc/salt/minion.d/minion.conf echo \u0026#34;startup_states: highstate\u0026#34; \u0026gt;\u0026gt; /etc/salt/minion.d/minion.conf systemctl enable salt-minion\u0026#39; # configure gpg pct exec 100 -- bash -c \u0026#39;echo \u0026#34;SALTMASTER_GPG_PUB\u0026#34; \u0026gt; /tmp/pubkey.asc\u0026#39; pct exec 100 -- bash -c \u0026#39;echo \u0026#34;SALTMASTER_GPG_PRIV\u0026#34; \u0026gt; /tmp/privkey.secret.b64\u0026#39; pct exec 100 -- bash -c \u0026#39;\\ mkdir -p /etc/salt/gpgkeys chmod 0700 /etc/salt/gpgkeys echo \u0026#39;homedir /etc/salt/gpgkeys\u0026#39; \u0026gt;\u0026gt; /root/.gnupg base64 -d /tmp/privkey.secret.b64 \u0026gt; /tmp/privkey.secret gpg --homedir /etc/salt/gpgkeys --import /tmp/privkey.secret gpg --homedir /etc/salt/gpgkeys --import /tmp/pubkey.asc echo \u0026#39;gpg_keydir: /etc/salt/gpgkeys\u0026#39; \u0026gt;/etc/salt/master.d/gpg-pillar.conf chown -R salt:salt /etc/gpgkeys rm /tmp/privkey.* rm /tmp/pubkey.*\u0026#39; pct reboot 100 # reload salt-minion to register correctly systemctl restart salt-minion fi Words in upper case like \u0026ldquo;SALTMASTER_ROOT_PWD\u0026rdquo; are injected by my custom build script during the ISO creation. As all rc.local files got deleted after the first connection to the saltmaster, I do not consider that as a big security concern.\nThe git repository used is a private one, which serves for GitOps via saltstack (more on that later).\nThe only remaining manual operations are listed below. After these actions, everything is set up and ready (including a fully functionnal k3s cluster with apps, again: more on that later):\nreboot all nodes to unsure that all configurations are taken into account\nadding node-2 to the cluster : it needs root password thus I can\u0026rsquo;t provide it in rc.local\npvecm add node-1.local.lan --link0 address=aaa.bbb.ccc.ddd add the qdevice on node-3 :\npvecm qdevice setup aaa.bbb.ccc.ddd apply saltstack map states (iac_backend.host-map.sls) : can\u0026rsquo;t do it before the cluster is created\nset saltmaster not to auto accept minions (auto_accept: false)\nSaltstack + Git as IAC frontend Concept As seen previously, the salt master is an LXC container that is automatically configured during the very first boot of node-3 (the Liva Z3). This makes it fully reproducible, eliminating the need to rely on backups. The goal is to quickly set up a salt master from scratch and use it to configure everything else, including the salt master itself.\nBasically, the concept is based on the following principles:\nA private Git repository contains all state and pillar definitions The repository is cloned regularly on the salt master LXC. Any file change in the cloned repository triggers a highstate application on all registered minions. Any new minion automatically applies a highstate upon registration. A highstate is periodically applied to all minions. This way, SaltStack acts as an Infrastructure as Code (IaC) frontend via Git: any push triggers changes in the infrastructure, removing the need for manual shell commands or direct connections to any server—whether it is an LXC, a virtual machine, or a Proxmox node.\nAdditionally, any new server is automatically configured, allowing me to rebuild parts or even the entire infrastructure with a simple salt '*' state.apply command.\nImplementation To implement the GitOps approach, one possible solution could have been to use GitFS to host SaltStack\u0026rsquo;s files. However, since I do not plan to make changes directly on the salt master (which, in my opinion, is an anti-pattern), I adopted a KISS approach: : a simple scheduled git pull task combined with an inotify-based state to apply any modifications.\nTo achieve this, the salt master has a scheduled task that pulls the repository every 2 minutes and a reactor configuration together with an inotify beacon.\nrefresh_saltstack_repo: schedule.present: - function: state.apply - job_args: - iac_frontend.repository - cron: \u0026#39;*/2 * * * *\u0026#39; - enabled: True salt-master: file.managed: - names: - /etc/salt/master.d/reactor.conf: - source: salt://iac_frontend/files/reactor.conf service.running: - watch: - file: /etc/salt/master.d/reactor.conf salt_states_changed: beacon.present: - save: True - enable: True - interval: 2 - beacon_module: inotify - disable_during_state_run: False - files: /srv: mask: - create - moved_to recurse: True auto_add: True exclude: - /srv/salt/reactor The repository pull state is like the following (url and credentials are stored on pillar values, more on that later):\nsaltstack_repo: git.latest: - name: https://path_to_git/repo - target: /srv - https_user: user - https_pass: password The \u0026ldquo;reactor.conf\u0026rdquo; file defines a state to be executed on each detected change:\n- \u0026#39;salt/beacon/*/salt_states_changed/*\u0026#39;: - salt://reactor/handle_changed_states.sls And here is the \u0026ldquo;handle_changed_states.sls\u0026rdquo; content which simply applies hightstate on all registered minions\napply_highstate: salt.state: - tgt: \u0026#39;*\u0026#39; - highstate: True As explained earlier, with such a simple mecanism, any push to the git repository triggers any infrastructure and configuration changes, no need to login into the saltmaster nor any server. Of course any error will also be deployed very fast : \u0026ldquo;with great power come great responsibilities\u0026rdquo; :)\nSecured secrets Even though my SaltStack repository is private, it is still hosted on external cloud servers. Since the entire architecture relies on secrets such as passwords or private keys, I needed to set up a secure way to store this kind of information.\nMy setup follows SaltStack\u0026rsquo;s approach, which consists of managing secrets with pillar values for the storage and GnuPG for encryption\nAs previously mentioned, the custom rc.local file of node-3, executed during the first boot, initializes the GnuPG environment and injects the key pair (which I store in a private local location).\nThus, the SaltMaster LXC contains everything needed to encrypt a new secret with a command like:\necho -n \u0026#39;value to encrypt\u0026#39; | gpg --homedir /etc/salt/gpgkeys --trust-model always -ear \u0026lt;MY KEY-ID\u0026gt; The output is an encrypted string that can be used as saltstack pillar:\n#!yaml|gpg standard_value: not encrypted encrypted_vlue: | -----BEGIN PGP MESSAGE----- PLb8RzQsA+XVp8SqaB/h2IsbSlwxC5auXxkJtQiZfeSJPVINAXIlT8F6KDRO5Aqe HaV2577PsEVRNeY9mMxPe0KVpuV3mPYL+2lpemEtwpJYDP1kByKMDiXt66sbyCNp v9lGMZI9ZnBsdGgLisZwDdaS0Vs+4MniIbw== -----END PGP MESSAGE----- Saltstack repository architecture I tried to follow the best pratices : pillar contains variables and customized values while states are mostly generics and depend on pillar\u0026rsquo;s values.\nBellow is a commented overview of the saltstack directory structure. Each element is detailled later.\nlevel 1 level 2 comments pillar k3s settings to deploy the k3s cluster mapsdefine all virtual machines and LXC specifications for all nodes servicessettings for specific services like reverse proxy (nginx) usersdefines users and groups that should exists or be deleted zabbixsettings dedicated to zabbix states git.slssettings for my saltstack git repository iac_backend.slsdefine sysctl values and custom scripts for all proxmox nodes kubeapps.slsdefine applications that must be deployed in the k3s cluster mail.slssettings for e-mail account and aliases for servers to be able to send e-mails top.sls salt iac_backend dedicated to proxmox backend deployments iac_frontenddedicated to saltstack and gitops kubeappsstates that deploy applications in k3s reactordeploy reactor configuration, essentialy for the gitops approach servicesdeploy services as defined in states sysadminstates dedicated to apply a standard configuration all servers, being LXC or VM usersensure that users and groups exist or are absents top.sls Proxmox nodes as IAC_backend Proxmox customization I did some adjustements on Proxmox nodes either to \u0026ldquo;optimize\u0026rdquo; things for my modest hardware, to reduce power consumption or simply to install my custom scripts\nThe table below show the optimization related settings.\nWhat How Allow swap usage only when less than 1% free RAM. This preserve my SSD set vm.swappiness to 1 disable ipv6 as I don’t use it set net.ipv6.conf.all.disable_ipv6 to 1 Ensure there is at least 256Mb free in order to always be able to execute sysadmin tools (ssh, screen, netstate, etc.) set vm.min_free_kbytes to 262144 Better I/O multitasking performances by limitting the size of writebacks (ram cache to disk) Tset vm.dirty_ratio to 20 These settings are applied through states and pillar values.\nPillar are defined like the follwing:\nClick to expand codeiac_backend: # custom scripts to install to /usr/loc/bin framework: - set_lxc.sh - set_vm.sh - set_templates.sh - set_common.sh - set_zfspv.sh # sysctl settings to set sysctl: vm.swappiness: 1 net.ipv6.conf.all.disable_ipv6: 1 vm.min_free_kbytes: 262144 vm.dirty_ratio: 20 And states applied to Promox nodes (more on udev rules in the next chapter):\nClick to expand code############################################################## # Deploy framework to handle IAC on Proxmox: # - define cluster hosts in /etc/hosts # - install requiered packages # - set a new rc.local to remove the one used during first boot # - install all custom script used to manage operations # - tune some parameters for better performances # - deploy a set of udevrules for better power management ############################################################## proxmox-cluster-hosts: file.managed: - name: /etc/hosts - source: salt://iac_backend/files/hosts - user: root - group: root - mode: 644 proxmox_packages: pkg.installed: - pkgs: - libguestfs-tools proxmox-rc-local: file.managed: - name: /etc/rc.local - source: salt://iac_backend/files/rc.local - user: root - group: root - mode: 750 {% if salt[\u0026#39;pillar.get\u0026#39;](\u0026#39;iac_backend:framework\u0026#39;, none) is not none %} proxmox_scripts: file.managed: - names: {% for file in pillar[\u0026#39;iac_backend\u0026#39;][\u0026#39;framework\u0026#39;] %} - /usr/local/bin/{{ file }}: - source: salt://iac_backend/files/{{ file }} - mode: 750 {%- endfor -%} {% endif %} {% if salt[\u0026#39;pillar.get\u0026#39;](\u0026#39;iac_backend:sysctl\u0026#39;, none) is not none %} proxmox_sysctl_settings: sysctl.present: - names: {% for setting in pillar[\u0026#39;iac_backend\u0026#39;][\u0026#39;sysctl\u0026#39;] %} - {{ setting }}: - value: {{ pillar[\u0026#39;iac_backend\u0026#39;][\u0026#39;sysctl\u0026#39;][setting] }} {%- endfor -%} {% endif %} proxmox_udev: cmd.run: - name: udevadm control --reload-rules - onchanges: - file: proxmox_udev_rules proxmox_udev_rules: file.managed: - names: - /etc/udev/rules.d/99-powermgmt.rules: - source: salt://iac_backend/files/99-powermgmt.rules - mode: 640 Power consumption One major advantage of my old ARM-based home lab solution was its low power consumption: it was around 60W, including my 24-port switch.\nSwitching to x86 required some BIOS tuning (see the beginning of this article), even with low-power CPUs. Unfortunately, I wasn’t able to achieve a huge reduction in power consumption. The initial power draw of the N100 nodes was about 19 watts. After applying BIOS tweaks and Linux adjustments, I managed to reduce it to 17.5W.\nHere are my \u0026ldquo;low-power\u0026rdquo; udev rules, installed in /etc/udev/rules.d/99-powermgmt.rules:\nACTION==\u0026#34;add\u0026#34;, SUBSYSTEM==\u0026#34;pci\u0026#34;, ATTR{power/control}=\u0026#34;auto\u0026#34; ACTION==\u0026#34;add\u0026#34;, SUBSYSTEM==\u0026#34;ahci\u0026#34;, ATTR{power/control}=\u0026#34;auto\u0026#34; ACTION==\u0026#34;add\u0026#34;, SUBSYSTEM==\u0026#34;scsi_host\u0026#34;, KERNEL==\u0026#34;host*\u0026#34;, ATTR{link_power_management_policy}=\u0026#34;min_power\u0026#34; ACTION==\u0026#34;add\u0026#34;, SUBSYSTEM==\u0026#34;usb\u0026#34;, ATTR{power/autosuspend_delay_ms}=\u0026#34;1000\u0026#34; ACTION==\u0026#34;add\u0026#34;, SUBSYSTEM==\u0026#34;usb\u0026#34;, ATTR{power/control}=\u0026#34;auto\u0026#34; ACTION==\u0026#34;add\u0026#34;, SUBSYSTEM==\u0026#34;scsi\u0026#34;, ATTR{power/control}=\u0026#34;auto\u0026#34; ACTION==\u0026#34;add\u0026#34;, SUBSYSTEM==\u0026#34;acpi\u0026#34;, ATTR{power/control}=\u0026#34;auto\u0026#34; ACTION==\u0026#34;add\u0026#34;, SUBSYSTEM==\u0026#34;block\u0026#34;, ATTR{power/control}=\u0026#34;auto\u0026#34; ACTION==\u0026#34;add\u0026#34;, SUBSYSTEM==\u0026#34;workqueue\u0026#34;, ATTR{power/control}=\u0026#34;auto\u0026#34; ACTION==\u0026#34;add\u0026#34;, SUBSYSTEM==\u0026#34;i2c\u0026#34;, ATTR{power/control}=\u0026#34;auto\u0026#34; With everything running, including the NL drives, ethernet switch, pfSense box, and rack cooling fans, I measured between 68W and 75W total. I found this reasonable: it\u0026rsquo;s a 25% increase in power consumption compared to my old ARM cluster, but with more than 25% performance gains. In the end, this translates to about 150 euros per year, which is much cheaper than a similar cloud service.\nDesired state configuration As mentioned earlier, I use Proxmox as my IAC backend. To have a system that allows defining LXC containers and virtual machines in a desired state configuration style, I had to write some scripts.\nI should have used salt-cloud, but at the time of writing, the current Proxmox extension isn\u0026rsquo;t very useful: it lacks reliable error reporting and is not yet officially integrated.\nTerraform or OpenTofu could have been good candidates, but they are too complex for a simple infrastructure like mine.\nMy approach is based on VMs, templates, and LXC definitions through pillar values, with deployment managed via states and custom scripts to handle both creation and modification.\nI wrote four scripts for VM and LXC creation/update. Their outputs follow SaltStack\u0026rsquo;s stateful script requirements.\nset_common.sh: Common tools and definitions for all scripts.\nClick to expand code#!/bin/bash ### COMMON VALUES ########################### DEBIAN12_CMD_INSTALL_SALTMINION=(\u0026#39;\\ wget --tries=3 --waitretry=3 --no-dns-cache --retry-on-host-error -O /etc/apt/keyrings/salt-archive-keyring.pgp https://packages.broadcom.com/artifactory/api/security/keypair/SaltProjectKey/public; \\ wget --tries=3 --waitretry=3 --no-dns-cache --retry-on-host-error -O /etc/apt/sources.list.d/salt.sources https://github.com/saltstack/salt-install-guide/releases/latest/download/salt.sources; \\ apt -qq update \u0026gt; /dev/null\u0026#39; ) DEBIAN12_CMD_CONFIG_SALTMINION=(\u0026#39;\\ mkdir -p /etc/salt/minion.d; \\ echo \u0026#34;master: saltmaster.local.lan\u0026#34; \u0026gt;\u0026gt; /etc/salt/minion.d/minion.conf; \\ systemctl enable salt-minion\u0026#39; ) ### GLOBAL VARIABLES ########################### CHANGED=\u0026#34;no\u0026#34; COMMENT=\u0026#34;\u0026#34; REPORT=\u0026#34;\u0026#34; SPECIAL_ARGS=\u0026#34;\u0026#34; MANDATORY=\u0026#34;\u0026#34; declare -A arguments declare -A specials ### FUNCTIONS ################################## # print a message that can be interpreted by saltstack cmd.script state # $1=exit code ExitMessage() { echo echo $REPORT echo \u0026#34;changed=$CHANGED comment=\u0026#39;$COMMENT\u0026#39;\u0026#34; exit $1 } # populate the arguments array waiting for key=value pairs # $1 are key exceptions that are mapped in $specials array # $2...$x all argumnts to parse ParseParameters() { # read parameters and store them in a hashmap for arg in \u0026#34;$@\u0026#34; do # separate argumente name and value (expecting name=value) name=${arg%%=*} value=${arg#*=} # extract special parameter for future usage if [[ $SPECIAL_ARGS == *\u0026#34;$name\u0026#34;* ]]; then specials[$name]=$value else # drop description as it causes problem to pass it to cli commands (quotes) if [[ $name != \u0026#39;description\u0026#39; ]]; then arguments[$name]=$value fi fi done } # check that mandatory parameters are set CheckMandatory() { for key in $MANDATORY do if [[ -z ${arguments[$key]} ]] \u0026amp;\u0026amp; [[ -z ${specials[$key]} ]] ; then COMMENT=\u0026#34;\u0026#39;$key\u0026#39; parameter is mandatory\u0026#34; ExitMessage 2 fi done } # parse a config output from proxmox cli command (qm config or pct config) # Call ConfigValue() function (defined in host script) for each parsed value. # # $1 = config string Parseconfig() { OIFS=\u0026#34;$IFS\u0026#34; IFS=$\u0026#39;\\n\u0026#39; for setting in $1 do name=${setting%%:*} value=${setting#*\u0026#39;: \u0026#39;} ConfigValue \u0026#34;$name\u0026#34; \u0026#34;$value\u0026#34; done IFS=\u0026#34;$OIFS\u0026#34; } set_lxc.sh: handle LXC creation and update\nClick to expand code#!/bin/bash source /usr/local/bin/set_common.sh ### GLOBAL VARIABLES ########################### CREATE=\u0026#34;\u0026#34; OSVER=\u0026#34;\u0026#34; SPECIAL_ARGS=\u0026#34;osver\u0026#34; MANDATORY=\u0026#34;id\u0026#34; ### FUNCTIONS ################################## Help() { # Display Help echo echo \u0026#34;Unsure a proxmox container exists as defined.\u0026#34; echo \u0026#34;Either create it, modify it or do nothing\u0026#34; echo echo \u0026#34;Syntax: $0 parameter1=value1 [paramter2=value2 [...] ]\u0026#34; echo \u0026#34;Parameters and values are passed directly to \u0026#39;pct\u0026#39;commands.\u0026#34; echo echo There is one special parameter for creation: echo \u0026#34; osver: defines the version of \u0026#39;ostype\u0026#39;. E.g: 12, base_20240911\u0026#34; echo echo IMPORTANTS NOTES: echo - description parameter is always dropped echo - rootfs parameter is ignored if container esists echo } # compare given value and set new config if changed # $1 = name # $2 = value ConfigValue() { # call pct set if anything changed, but for rootfs setting if [[ ! -z ${arguments[$1]} ]] \u0026amp;\u0026amp; [[ ${arguments[$1]} != $2 ]] \u0026amp;\u0026amp; [[ $1 != \u0026#34;rootfs\u0026#34; ]] ; then pct set ${arguments[\u0026#39;id\u0026#39;]} --$1 \u0026#34;${arguments[$1]}\u0026#34; CHANGED=\u0026#34;yes\u0026#34; REPORT=\u0026#34;$REPORT $1=\u0026#39;${arguments[$1]}\u0026#39;\u0026#34; fi if [[ $CHANGED == \u0026#34;yes\u0026#34; ]]; then COMMENT=\u0026#34;Container ${arguments[id]} updated\u0026#34; else COMMENT=\u0026#34;Container ${arguments[id]} already configured\u0026#34; fi } ### SCRIPT LOGIC ################################ # Display help if needed if [[ ($# -lt 1) || $1 == \u0026#34;help\u0026#34; ]] ; then Help exit 2 fi ParseParameters \u0026#34;$@\u0026#34; CheckMandatory # check if container exists if [[ \u0026#34;$(pct list | grep ${arguments[\u0026#39;id\u0026#39;]} | awk \u0026#39;{print $1}\u0026#39;)\u0026#34; -eq ${arguments[\u0026#39;id\u0026#39;]} ]]; then Parseconfig \u0026#34;$(pct config ${arguments[\u0026#39;id\u0026#39;]})\u0026#34; # when container does not exists we create it else # to create a container, we need osver (e.g. 12) and ostype (e.g. debian) to find the required template (e.g. debian-12) if [[ -z ${specials[\u0026#39;osver\u0026#39;]} ]] || [[ -z ${arguments[\u0026#39;ostype\u0026#39;]} ]] ; then COMMENT=\u0026#34;osver and ostype parameters are mandatory for non existing container\u0026#34; ExitMessage 2 fi # build \u0026#39;pct create\u0026#39; arguments as well as saltstack report for cmd.script state for param in ${!arguments[@]} do if [[ $param != \u0026#39;id\u0026#39; ]]; then CREATE=\u0026#34;$CREATE --$param ${arguments[$param]}\u0026#34; REPORT=\u0026#34;$REPORT $param=\u0026#39;${arguments[$param]}\u0026#39;\u0026#34; fi done COMMENT=\u0026#34;Container ${arguments[\u0026#39;id\u0026#39;]} created with\u0026#34; # get template filename or download it if not found TEMPLATE=$(pveam available --section system | grep \u0026#34;${arguments[\u0026#39;ostype\u0026#39;]}-${specials[\u0026#39;osver\u0026#39;]}\u0026#34; | awk \u0026#39;{print $2}\u0026#39;) TEMPLATE_PATH=\u0026#34;/var/lib/vz/template/cache\u0026#34; TEMPLATE_STORAGE_NAME=\u0026#34;local\u0026#34; # define wether to use default local storage of zfs if available if [[ -d \u0026#34;/zfs-storage/vz\u0026#34; ]]; then TEMPLATE_PATH=\u0026#34;/zfs-storage/vz/template/cache\u0026#34; TEMPLATE_STORAGE_NAME=\u0026#34;vz\u0026#34; fi if [[ ! -f \u0026#34;$TEMPLATE_PATH/$TEMPLATE\u0026#34; ]]; then pveam download $TEMPLATE_STORAGE_NAME $TEMPLATE \u0026gt; /dev/null COMMENT=\u0026#34;$COMMENT new template: $TEMPLATE\u0026#34; else COMMENT=\u0026#34;$COMMENT existing template\u0026#34; fi # create container pct create ${arguments[\u0026#39;id\u0026#39;]} $TEMPLATE_PATH/$TEMPLATE $CREATE \u0026gt; /dev/null # if container has been created, install salt-minion (depending on OS) if [[ $? -eq 0 ]]; then CHANGED=\u0026#34;yes\u0026#34; if [[ \u0026#34;${arguments[\u0026#39;ostype\u0026#39;]}-${specials[\u0026#39;osver\u0026#39;]}\u0026#34; == \u0026#34;debian-12\u0026#34; ]]; then # set salt-minion requirements pct exec ${arguments[\u0026#39;id\u0026#39;]} -- bash -c \u0026#34;${DEBIAN12_CMD_INSTALL_SALTMINION[@]}\u0026#34; # upgrade and install needed packages pct exec ${arguments[\u0026#39;id\u0026#39;]} -- bash -c \u0026#34;apt -yqq upgrade \u0026gt; /dev/null \u0026amp;\u0026amp; apt -yqq install salt-minion \u0026gt; /dev/null\u0026#34; # configure saltminion pct exec ${arguments[\u0026#39;id\u0026#39;]} -- bash -c \u0026#34;${DEBIAN12_CMD_CONFIG_SALTMINION[@]}\u0026#34; # reboot container to make it ready pct reboot ${arguments[\u0026#39;id\u0026#39;]} COMMENT=\u0026#34;$COMMENT. Salt-minion installed\u0026#34; fi else exit 1 fi fi # print saltstack readble message then exit with success ExitMessage 0 set_templates.sh: handle Proxmox VM templates\nClick to expand code#!/bin/bash source /usr/local/bin/set_common.sh ### GLOBAL VARIABLES ########################### MANDATORY=\u0026#34;id name image\u0026#34; ### FUNCTIONS ################################## Help() { # Display Help echo echo \u0026#34;Unsure a VM Template is present.\u0026#34; echo \u0026#34;Either create it or do nothing\u0026#34; echo echo \u0026#34;Syntax: $0 id=\u0026lt;template id\u0026gt; name=\u0026lt;template name\u0026gt; image=\u0026lt;http link to cloudinit image\u0026gt; [ param=value ]\u0026#34; echo \u0026#34;Where param=value can be any \u0026#34;qm set\u0026#34; / \u0026#34;wm create\u0026#34; attributes\u0026#34; echo echo \u0026#34;NOTE: Created template will have guest-agent and salt-minion installed\u0026#34; echo } ### SCRIPT LOGIC ################################ # Display help if needed if [[ ($# -lt 3) || $1 == \u0026#34;help\u0026#34; ]] ; then Help exit 2 fi ParseParameters \u0026#34;$@\u0026#34; CheckMandatory cd /root # check if template exists if [[ \u0026#34;$(qm list | grep ${arguments[\u0026#39;id\u0026#39;]} | awk \u0026#39;{print $1}\u0026#39;)\u0026#34; -eq ${arguments[\u0026#39;id\u0026#39;]} ]]; then COMMENT=\u0026#34;Image with ID ${arguments[\u0026#39;id\u0026#39;]} already exists\u0026#34; ExitMessage 0 fi # download image wget --tries=3 --waitretry=3 --no-dns-cache --retry-on-host-error -nv -c -O \u0026#34;temp.img\u0026#34; ${arguments[\u0026#39;image\u0026#39;]} if [[ ! $? -eq 0 ]]; then COMMENT=\u0026#34;Unable to download image ${arguments[\u0026#39;image\u0026#39;]}\u0026#34; ExitMessage 1 else COMMENT=\u0026#34;New template created with ID ${arguments[\u0026#39;id\u0026#39;]} (${arguments[\u0026#39;name\u0026#39;]})\u0026#34; fi # install saltstack on supported images if [[ ${arguments[\u0026#39;image\u0026#39;]} == *\u0026#34;debian-12\u0026#34;* ]]; then virt-customize -a temp.img --run-command \u0026#34;${DEBIAN12_CMD_INSTALL_SALTMINION[@]}\u0026#34; virt-customize -a temp.img --install salt-minion if [[ ! $? -eq 0 ]]; then COMMENT=\u0026#34;Unable to install salt-minion on debian-12 image\u0026#34; ExitMessage 1 fi virt-customize -a temp.img --run-command \u0026#34;salt-pip install croniter\u0026#34; virt-customize -a temp.img --run-command \u0026#34;${DEBIAN12_CMD_CONFIG_SALTMINION[@]}\u0026#34; COMMENT=\u0026#34;$COMMENT. Salt-minion installed\u0026#34; fi # install guest agent virt-customize -a temp.img --install qemu-guest-agent virt-customize -a temp.img --run-command \u0026#39;systemctl enable qemu-guest-agent\u0026#39; if [[ ! $? -eq 0 ]]; then COMMENT=\u0026#34;Unable to install qemu-guest-agent\u0026#34; ExitMessage 1 fi # generate the template qm create ${arguments[\u0026#39;id\u0026#39;]} --memory 2048 --core 2 --name ${arguments[\u0026#39;name\u0026#39;]} --net0 virtio,bridge=vmbr0 qm importdisk ${arguments[\u0026#39;id\u0026#39;]} temp.img storage \u0026gt; /dev/null qm set ${arguments[\u0026#39;id\u0026#39;]} --scsihw virtio-scsi-pci \\ --scsi0 storage:vm-${arguments[\u0026#39;id\u0026#39;]}-disk-0 \\ --ide2 storage:cloudinit \\ --boot c --bootdisk scsi0 \\ --serial0 socket --vga serial0 \\ --agent enabled=1 qm template ${arguments[\u0026#39;id\u0026#39;]} rm /root/temp.img CHANGED=\u0026#34;yes\u0026#34; ExitMessage 0 set_vm.sh: handle virtual machines creation and update\nClick to expand code#!/bin/bash source /usr/local/bin/set_common.sh ### GLOBAL VARIABLES ########################### SPECIAL_ARGS=\u0026#34;sshkey id name template start\u0026#34; MANDATORY=\u0026#34;id name template\u0026#34; ### FUNCTIONS ################################## Help() { # Display Help echo echo \u0026#34;Unsure a proxmox VM exists as defined.\u0026#34; echo \u0026#34;Either create it, modify it or do nothing\u0026#34; echo echo \u0026#34;Syntax: $0 id=\u0026lt;id\u0026gt; name=\u0026lt;name\u0026gt; template=\u0026lt;id\u0026gt; [start=\u0026lt;0 | 1\u0026gt;] parameter1=value1 [paramter2=value2 [...] ]\u0026#34; echo \u0026#34;Parameters and values are passed directly to \u0026#39;qm\u0026#39; commands.\u0026#34; echo echo \u0026#34;IMPORTANTS NOTES:\u0026#34; echo \u0026#34; - description parameter is always dropped\u0026#34; echo \u0026#34; - sshkey is ignored and always defaulted to ~/.ssh/id_rsa.pub\u0026#34; echo } # compare given value and set new config if changed # $1 = name # $2 = value ConfigValue() { # call pct set if anything changed, but for rootfs setting if [[ ! -z ${arguments[$1]} ]] \u0026amp;\u0026amp; [[ ${arguments[$1]} != $2 ]] ; then # special handling for scsi0: only size is changeable if [[ $1 == \u0026#34;scsi0\u0026#34; ]]; then disksize=$(echo \u0026#34;${arguments[$1]}\u0026#34; | \\grep -oP \u0026#39;(?\u0026lt;=size\\=).*(?=G)\u0026#39;) qm resize ${specials[\u0026#39;id\u0026#39;]} scsi0 \u0026#34;${disksize}G\u0026#34; else qm set ${specials[\u0026#39;id\u0026#39;]} --$1 \u0026#34;${arguments[$1]}\u0026#34; fi CHANGED=\u0026#34;yes\u0026#34; REPORT=\u0026#34;$REPORT $1=\u0026#39;${arguments[$1]}\u0026#39;\u0026#34; fi if [[ $CHANGED == \u0026#34;yes\u0026#34; ]]; then COMMENT=\u0026#34;VM ${specials[id]} updated\u0026#34; else COMMENT=\u0026#34;VM ${specials[id]} already configured\u0026#34; fi } ### SCRIPT LOGIC ################################ # Display help if needed if [[ ($# -lt 3) || $1 == \u0026#34;help\u0026#34; ]] ; then Help exit 2 fi ParseParameters \u0026#34;$@\u0026#34; CheckMandatory # check if vm exists if [[ \u0026#34;$(qm list | grep ${specials[\u0026#39;id\u0026#39;]} | awk \u0026#39;{print $1}\u0026#39;)\u0026#34; -eq ${specials[\u0026#39;id\u0026#39;]} ]]; then Parseconfig \u0026#34;$(qm config ${specials[\u0026#39;id\u0026#39;]})\u0026#34; else # clone template qm clone ${specials[\u0026#39;template\u0026#39;]} ${specials[\u0026#39;id\u0026#39;]} --name ${specials[\u0026#39;name\u0026#39;]} if [[ ! $? -eq 0 ]]; then COMMENT=\u0026#34;Error cloning template\u0026#34; ExitMessage 1 fi CHANGED=\u0026#34;yes\u0026#34; # set ssh key qm set ${specials[\u0026#39;id\u0026#39;]} --sshkey ~/.ssh/id_rsa.pub # apply all setings from argumemts for param in ${!arguments[@]} do if [[ $param == \u0026#34;scsi0\u0026#34; ]]; then disksize=$(echo \u0026#34;${arguments[$param]}\u0026#34; | \\grep -oP \u0026#39;(?\u0026lt;=size\\=).*(?=G)\u0026#39;) qm resize ${specials[\u0026#39;id\u0026#39;]} scsi0 \u0026#34;${disksize}G\u0026#34; else qm set ${specials[\u0026#39;id\u0026#39;]} --$param ${arguments[$param]} fi REPORT=\u0026#34;$REPORT $param=\u0026#39;${arguments[$param]}\u0026#39;\u0026#34; done if [[ ! -z ${specials[\u0026#39;start\u0026#39;]} ]] \u0026amp;\u0026amp; [[ ${specials[\u0026#39;start\u0026#39;]} -eq 1 ]] ; then qm start ${specials[\u0026#39;id\u0026#39;]} if [[ $? -eq 0 ]]; then COMMENT=\u0026#34;$COMMENT. VM started\u0026#34; else COMMENT=\u0026#34;$COMMENT. Error trying to start the VM\u0026#34; fi fi fi # print saltstack readble message then exit with success ExitMessage 0 Below is how I defined templates, LXC and VM as pillar values to create and/or update them:\nClick to expand codetemplates: tpl-debian-12: \u0026gt; id=9200 name=debian-12 image=https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2 map: lxc-goldorak: password: | -----BEGIN PGP MESSAGE----- [...] -----END PGP MESSAGE----- settings: \u0026gt; id=300 hostname=goldorak cores=2 memory=1024 rootfs=storage:6 net0=name=eth0,bridge=vmbr0,ip=dhcp,tag=2,type=veth ostype=debian osver=12 onboot=1 swap=0 features=nesting=1 start=1 vm-k3s-master-1: password: | -----BEGIN PGP MESSAGE----- [...] -----END PGP MESSAGE----- settings: \u0026gt; id=1200 name=k3s-master-1 template=9200 cores=4 scsi0=storage:base-9200-disk-0/vm-1200-disk-0,size=20G memory=12288 net0=model=virtio,bridge=vmbr0,tag=3 ipconfig0=ip=dhcp searchdomain=local.lan onboot=1 start=1 Now, the part of the states that handle these pillar values:\nClick to expand code# handle VM templates {% if pillar.get(\u0026#39;templates\u0026#39;, none) is not none %} {% for id in pillar[\u0026#39;templates\u0026#39;] %} {{ id }}: cmd.script: - name: /usr/local/bin/set_templates.sh - stateful: True - require: - file: proxmox_scripts - pkg: proxmox_packages - args: {{ pillar[\u0026#39;templates\u0026#39;][id] }} {% endfor %} {% endif %} # handle VM and LXC creations and updates {% if pillar.get(\u0026#39;map\u0026#39;, none) is not none %} {% for id in pillar[\u0026#39;map\u0026#39;] %} {% if id.startswith(\u0026#39;lxc\u0026#39;) %} {% set password=\u0026#39;password=\u0026#39; %} {% set name=\u0026#39;set_lxc.sh\u0026#39; %} {% elif id.startswith(\u0026#39;vm\u0026#39;) %} {% set password=\u0026#39;cipassword=\u0026#39; %} {% set name=\u0026#39;set_vm.sh\u0026#39; %} {% endif %} {% set password=password~pillar[\u0026#39;map\u0026#39;][id][\u0026#39;password\u0026#39;] %} {{ id }}: cmd.script: - name: /usr/local/bin/{{ name }} - stateful: True - require: - file: proxmox_scripts - pkg: proxmox_packages - args: \u0026gt; {{ password }} {{ pillar[\u0026#39;map\u0026#39;][id][\u0026#39;settings\u0026#39;] }} {% endfor %} {% endif %} User management My user management needs are pretty basic: system accounts and Samba shares. A simpler alternative to LDAP for my home lab is using states and minion values to handle all operations (create, modify, delete) and synchronize them across all hosts.\nExample of user definitions in the pillar:\nClick to expand code#!yaml|gpg # if defined AND set to true, this setting is used to trigger # samba password handling. Else, only system password are set pdbedit: True revokedusers: - debian users: test: fullname: full test uid: 1000 shell: /bin/bash ssh-keys: - ssh-ed25519 [...] test@my_computer groups: - adm - plugdev - sudo - staff - my_group password: | -----BEGIN PGP MESSAGE----- [...] -----END PGP MESSAGE----- groups: my_group: gid: 1003 States to handle users on all nodes:\nClick to expand code############################################################## # Handle users on all managed hosts # - Remove users present in \u0026#39;revokedusers\u0026#39; pillar (including ssh key) # - Add groups present in \u0026#39;groups\u0026#39; pillar # - Add users present in \u0026#39;users\u0026#39; pillar (including ssh key) # - Handle samba passwords if needed (\u0026#39;pdbedit\u0026#39; pillar) ############################################################## # delete users and groups that have been revoked # unsure user is no more referenced {% if pillar.get(\u0026#39;revokedusers\u0026#39;, none) is not none %} {% for user in pillar[\u0026#39;revokedusers\u0026#39;] %} {{user}}: user.absent: [] group.absent: [] # unsure user ssh key is no more used by root {% if salt[\u0026#39;pillar.get\u0026#39;](\u0026#39;revokedusers:ssh-keys\u0026#39;, none) is not none %} {{user}}_root_key: ssh_auth.absent: - user: root - names: {% for key in pillar[\u0026#39;revokedusers\u0026#39;][\u0026#39;ssh-keys\u0026#39;] %} - {{ key }} {% endfor %} # unsure user ssh key is no more used for this user {{user}}_key: ssh_auth.absent: - user: {{user}} - names: {% for key in pillar[\u0026#39;revokedusers\u0026#39;][\u0026#39;ssh-keys\u0026#39;] %} - {{ key }} {% endfor %} {% endif %} {% endfor %} {% endif %} # set groups that must be defined {% if pillar.get(\u0026#39;groups\u0026#39;, none) is not none %} {% for group in pillar[\u0026#39;groups\u0026#39;] %} {{ group }}: group.present: - gid: {{ pillar[\u0026#39;groups\u0026#39;][group][\u0026#39;gid\u0026#39;] }} {% endfor %} {% endif %} # Set users that must be defined {% if pillar.get(\u0026#39;users\u0026#39;, none) is not none %} {% for user in pillar[\u0026#39;users\u0026#39;] %} {{ user }}: group.present: - gid: {{ pillar[\u0026#39;users\u0026#39;][user][\u0026#39;uid\u0026#39;] }} user.present: - fullname: {{ pillar[\u0026#39;users\u0026#39;][user][\u0026#39;fullname\u0026#39;] }} - uid: {{ pillar[\u0026#39;users\u0026#39;][user][\u0026#39;uid\u0026#39;] }} - gid: {{ pillar[\u0026#39;users\u0026#39;][user][\u0026#39;uid\u0026#39;] }} - shell: {{ pillar[\u0026#39;users\u0026#39;][user][\u0026#39;shell\u0026#39;] }} - password: {{ pillar[\u0026#39;users\u0026#39;][user][\u0026#39;password\u0026#39;] }} - hash_password: True {% if salt[\u0026#39;pillar.get\u0026#39;](\u0026#39;users:\u0026#39; + user + \u0026#39;:groups\u0026#39;, none) is not none -%} - groups: {% for group in pillar[\u0026#39;users\u0026#39;][user][\u0026#39;groups\u0026#39;] -%} - {{ group }} {% endfor %} {% endif %} {% if pillar.get(\u0026#39;pdbedit\u0026#39;, none) is not none %} pdbedit.managed: - password: {{ pillar[\u0026#39;users\u0026#39;][user][\u0026#39;password\u0026#39;] }} {% endif %} {% if salt[\u0026#39;pillar.get\u0026#39;](\u0026#39;users:\u0026#39; + user + \u0026#39;:ssh-keys\u0026#39;, none) is not none -%} {{user}}_root_key: ssh_auth.present: - user: root - names: {% for key in pillar[\u0026#39;users\u0026#39;][user][\u0026#39;ssh-keys\u0026#39;] -%} - {{ key }} {% endfor %} {{user}}_key: ssh_auth.present: - user: {{user}} - names: {% for key in pillar[\u0026#39;users\u0026#39;][user][\u0026#39;ssh-keys\u0026#39;] -%} - {{ key }} {% endfor %} {% endif %} {% endfor %} {% endif %} Zabbix as monitoring solution I chose Zabbix to monitor all components of my home cloud. I found it easy to set up, yet very powerful, with many useful monitors by default.\nI created a single state for the installation process, both for the server and all agents, depending on pillar values. I also had to write a small script to handle PostgreSQL database creation.\nExample of pillar values:\nClick to expand code#!yaml|gpg zabbix-common: server: \u0026#39;monitor.local.lan\u0026#39; repo: \u0026#39; https://repo.zabbix.com/zabbix/7.0/debian/pool/main/z/zabbix-release/zabbix-release_latest+debian12_all.deb\u0026#39; zabbix-server: db_name: \u0026#39;zabbix\u0026#39; db_user: \u0026#39;zabbix\u0026#39; db_script: \u0026#39;/usr/share/zabbix-sql-scripts/postgresql/server.sql.gz\u0026#39; db_password: | -----BEGIN PGP MESSAGE----- [...] -----END PGP MESSAGE----- States that install Zabbix:\nClick to expand code############################################################## # Deploy Zabbix server or agent regarding zabbix-* pillars. # ALL: # - install the repo # - ensure all required services are running # # SERVER: # - Init PostgreSQL database # - install all needed packages # - customize configuration files with db, user and password values # - set nginx to use servername defined in \u0026#39;server\u0026#39; pillar and to listen to port 80 # # AGENT: # - Deploy customized configuration files with server fqdn (or 127.0.0.1 on zabbix server) # ############################################################## # zabbix repository zabbix-repo-install: cmd.run: - name: \u0026#34;wget -O /tmp/zabbix-release.deb {{ pillar[\u0026#39;zabbix-common\u0026#39;][\u0026#39;repo\u0026#39;] }} \u0026amp;\u0026amp; dpkg -i /tmp/zabbix-release.deb \u0026amp;\u0026amp; apt-get update\u0026#34; - unless: \u0026#39;dpkg -l | grep zabbix-release\u0026#39; # zabbix server {% if pillar.get(\u0026#39;zabbix-server\u0026#39;, none) is not none %} zabbix-set-locale: file.line: - name: /etc/locale.gen - match: \u0026#39;en_US.UTF-8 UTF-8\u0026#39; - mode: replace - content: \u0026#39;en_US.UTF-8 UTF-8\u0026#39; cmd.run: - name: locale-gen zabbix-set-default-locale: file.line: - name: /etc/default/locale - match: \u0026#39;LANG=\u0026#39; - mode: replace - content: \u0026#39;LANG=en_US.UTF-8\u0026#39; zabbix-set_pgsql: file.managed: - name: /usr/local/bin/set_pgsql.sh - source: salt://services/files/set_pgsql.sh - user: root - group: root - mode: 750 zabbix-server-install: pkg.installed: - pkgs: - zabbix-server-pgsql - zabbix-frontend-php - php8.2-pgsql - zabbix-nginx-conf - zabbix-sql-scripts - postgresql-all - require: - cmd: zabbix-repo-install zabbix-postgresql: service.running: - name: postgresql - enable: True - require: - pkg: zabbix-server-install zabbix-db-user-creation: cmd.script: - name: /usr/local/bin/set_pgsql.sh - stateful: True - require: - file: zabbix-set_pgsql - pkg: zabbix-server-install - args: {{ pillar[\u0026#39;zabbix-server\u0026#39;][\u0026#39;db_name\u0026#39;] }} {{ pillar[\u0026#39;zabbix-server\u0026#39;][\u0026#39;db_user\u0026#39;] }} {{ pillar[\u0026#39;zabbix-server\u0026#39;][\u0026#39;db_password\u0026#39;] }} {{ pillar[\u0026#39;zabbix-server\u0026#39;][\u0026#39;db_script\u0026#39;] }} /etc/zabbix/zabbix_server.conf: file.managed: - source: salt://services/files/zabbix_server.conf - template: jinja - context: DBPassword: {{ pillar[\u0026#39;zabbix-server\u0026#39;][\u0026#39;db_password\u0026#39;] }} DBName: {{ pillar[\u0026#39;zabbix-server\u0026#39;][\u0026#39;db_name\u0026#39;] }} DBUser: {{ pillar[\u0026#39;zabbix-server\u0026#39;][\u0026#39;db_user\u0026#39;] }} - require: - pkg: zabbix-server-install /etc/zabbix/web/zabbix.conf.php: file.managed: - source: salt://services/files/zabbix.conf.php - template: jinja - context: DBPassword: {{ pillar[\u0026#39;zabbix-server\u0026#39;][\u0026#39;db_password\u0026#39;] }} DBName: {{ pillar[\u0026#39;zabbix-server\u0026#39;][\u0026#39;db_name\u0026#39;] }} DBUser: {{ pillar[\u0026#39;zabbix-server\u0026#39;][\u0026#39;db_user\u0026#39;] }} - user: www-data - group: www-data - mode: 600 - require: - pkg: zabbix-server-install zabbix-nginx.conf-port: file.line: - name: /etc/zabbix/nginx.conf - match: \u0026#39;8080\u0026#39; - mode: replace - content: \u0026#39;listen 80;\u0026#39; - require: - pkg: zabbix-server-install zabbix-nginx.conf-host: file.line: - name: /etc/zabbix/nginx.conf - match: \u0026#39;example.com\u0026#39; - mode: replace - content: \u0026#39;server_name {{ pillar[\u0026#39;zabbix-common\u0026#39;][\u0026#39;server\u0026#39;] }};\u0026#39; - require: - pkg: zabbix-server-install zabbix-nginx-default-config: file.absent: - name: /etc/nginx/sites-enabled/default - require: - pkg: zabbix-server-install zabbix-server: service.running: - enable: True - require: - pkg: zabbix-server-install zabbix-server-nginx: service.running: - name: nginx - enable: True - require: - pkg: zabbix-server-install zabbix-server-php: service.running: - name: php8.2-fpm - enable: True - require: - pkg: zabbix-server-install {% endif %} # zabbix agent {% if pillar.get(\u0026#39;zabbix-common\u0026#39;, none) is not none %} zabbix-agent-install: pkg.installed: - pkgs: - zabbix-agent2 - require: - cmd: zabbix-repo-install /etc/zabbix/zabbix_agent2.conf: file.managed: - source: salt://services/files/zabbix_agent2.conf - template: jinja - context: {% if pillar.get(\u0026#39;zabbix-server\u0026#39;, none) is none %} server: {{ pillar[\u0026#39;zabbix-common\u0026#39;][\u0026#39;server\u0026#39;] }} {% else %} server: 127.0.0.1 {% endif %} - require: - pkg: zabbix-agent-install zabbix-agent2: service.running: - enable: True - require: - file: /etc/zabbix/zabbix_agent2.conf - watch: - file: /etc/zabbix/zabbix_agent2.conf {% endif %} As you can see, these state definitions rely on some external files.\nzabbix.conf.php : jinja template for zabbix configuration\nClick to expand code\u0026lt;?php // Zabbix GUI configuration file. $DB[\u0026#39;TYPE\u0026#39;] = \u0026#39;POSTGRESQL\u0026#39;; $DB[\u0026#39;SERVER\u0026#39;] = \u0026#39;localhost\u0026#39;; $DB[\u0026#39;PORT\u0026#39;] = \u0026#39;0\u0026#39;; $DB[\u0026#39;DATABASE\u0026#39;] = \u0026#39;{{ DBName }}\u0026#39;; $DB[\u0026#39;USER\u0026#39;] = \u0026#39;{{ DBUser }}\u0026#39;; $DB[\u0026#39;PASSWORD\u0026#39;] = \u0026#39;{{ DBPassword }}\u0026#39;; // Schema name. Used for PostgreSQL. $DB[\u0026#39;SCHEMA\u0026#39;] = \u0026#39;\u0026#39;; // Used for TLS connection. $DB[\u0026#39;ENCRYPTION\u0026#39;] = true; $DB[\u0026#39;KEY_FILE\u0026#39;] = \u0026#39;\u0026#39;; $DB[\u0026#39;CERT_FILE\u0026#39;] = \u0026#39;\u0026#39;; $DB[\u0026#39;CA_FILE\u0026#39;] = \u0026#39;\u0026#39;; $DB[\u0026#39;VERIFY_HOST\u0026#39;] = false; $DB[\u0026#39;CIPHER_LIST\u0026#39;] = \u0026#39;\u0026#39;; // Vault configuration. Used if database credentials are stored in Vault secrets manager. $DB[\u0026#39;VAULT\u0026#39;] = \u0026#39;\u0026#39;; $DB[\u0026#39;VAULT_URL\u0026#39;] = \u0026#39;\u0026#39;; $DB[\u0026#39;VAULT_PREFIX\u0026#39;] = \u0026#39;\u0026#39;; $DB[\u0026#39;VAULT_DB_PATH\u0026#39;] = \u0026#39;\u0026#39;; $DB[\u0026#39;VAULT_TOKEN\u0026#39;] = \u0026#39;\u0026#39;; $DB[\u0026#39;VAULT_CERT_FILE\u0026#39;] = \u0026#39;\u0026#39;; $DB[\u0026#39;VAULT_KEY_FILE\u0026#39;] = \u0026#39;\u0026#39;; // Uncomment to bypass local caching of credentials. // $DB[\u0026#39;VAULT_CACHE\u0026#39;] = true; // Uncomment and set to desired values to override Zabbix hostname/IP and port. // $ZBX_SERVER = \u0026#39;\u0026#39;; // $ZBX_SERVER_PORT = \u0026#39;\u0026#39;; $ZBX_SERVER_NAME = \u0026#39;{{ grains[\u0026#39;host\u0026#39;] }}\u0026#39;; zabbix_server.conf: jinja template for Zabbix server configuration\nClick to expand codeLogFile=/var/log/zabbix/zabbix_server.log PidFile=/run/zabbix/zabbix_server.pid SocketDir=/run/zabbix DBName={{ DBName }} DBUser={{ DBUser }} DBPassword={{ DBPassword }} SNMPTrapperFile=/var/log/snmptrap/snmptrap.log Timeout=20 FpingLocation=/usr/bin/fping Fping6Location=/usr/bin/fping6 LogSlowQueries=3000 StatsAllowedIP=127.0.0.1 EnableGlobalScripts=0 zabbix_agent2.conf: jinja template for all Zabbix agents configuration. Note that there is a subtle settings here. If the host use kubernetes persistent volumes (detected with \u0026lsquo;k3s-labels:pv\u0026rsquo;), all vfs triggers are disabled. I did that because else, Zabbix agent would have mounted the persistent volumes on all nodes at the same same, which would creates corruption on the filesystem (more on persistent volumes later).\nClick to expand codeServer={{ server }} ServerActive={{ server }} Hostname={{ grains[\u0026#39;host\u0026#39;] }} LogFile=/var/log/zabbix/zabbix_agent2.log PidFile=/run/zabbix/zabbix_agent2.pid PluginSocket=/run/zabbix/zabbix_agent2.sock {% if salt.pillar.get(\u0026#39;k3s-labels:pv\u0026#39;, none) == \u0026#39;enabled\u0026#39; -%} DenyKey=vfs.*[*] {% endif %} Here is the custom script that handle PostgreSQL database. It it compliant with saltstack\u0026rsquo;s stateful script requirements:\nClick to expand code#!/bin/bash ### GLOBAL VARIABLES ########################### CHANGED=\u0026#34;no\u0026#34; COMMENT=\u0026#34;\u0026#34; DBNAME=\u0026#34;\u0026#34; USER=\u0026#34;\u0026#34; PASSWD=\u0026#34;\u0026#34; ### FUNCTIONS ################################## Help() { # Display Help echo echo \u0026#34;Unsure a PostgreSQL DB and user exists\u0026#34; echo \u0026#34;Either create it, modify it or do nothing\u0026#34; echo echo \u0026#34;Syntax: $0 \u0026lt;dbname\u0026gt; \u0026lt;user\u0026gt; \u0026lt;password\u0026gt; [sql_script.gz]\u0026#34; echo echo \u0026#34;IMPORTANTS NOTES:\u0026#34; echo \u0026#34; Mind that any givn password can be shown in process list for a short time and can also be storedin bash history\u0026#34; echo } # print a message that can be interpreted by saltstack cmd.script state # $1=exit code ExitMessage() { echo echo \u0026#34;changed=$CHANGED comment=\u0026#39;$COMMENT\u0026#39;\u0026#34; exit $1 } ### SCRIPT LOGIC ################################ # check if a record exists in given table return 1 if found # $1: table # $2: field to use for earching (where) # $3: record to search Pg_Record_exists() { table=\u0026#34;$1\u0026#34; field=\u0026#34;$2\u0026#34; record=\u0026#34;$3\u0026#34; sudo -u postgres psql -tc \u0026#34;SELECT 1 FROM $table WHERE $field = \u0026#39;$record\u0026#39;\u0026#34; } # Display help if needed if [[ ($# -lt 3) || $1 == \u0026#34;help\u0026#34; ]] ; then Help exit 2 fi DBNAME=\u0026#34;$1\u0026#34; USER=\u0026#34;$2\u0026#34; PASSWD=\u0026#34;$3\u0026#34; if [[ \u0026#34;$(Pg_Record_exists pg_roles rolname $USER | awk \u0026#39;{print $1}\u0026#39;)\u0026#34; -eq 1 ]]; then COMMENT=\u0026#34;User $USER already exists.\u0026#34; else sudo -u postgres psql -c \u0026#34;CREATE ROLE $USER LOGIN PASSWORD \u0026#39;$PASSWD\u0026#39;;\u0026#34; CHANGED=\u0026#34;yes\u0026#34; COMMENT=\u0026#34;User $USER created.\u0026#34; fi if [[ \u0026#34;$(Pg_Record_exists pg_database datname $DBNAME | awk \u0026#39;{print $1}\u0026#39;)\u0026#34; -eq 1 ]]; then COMMENT=\u0026#34;$COMMENT Database $DBNAME already exists.\u0026#34; else sudo -u postgres psql -c \u0026#34;CREATE DATABASE $DBNAME OWNER $USER LOCALE \u0026#39;en_US.UTF-8\u0026#39; ENCODING UTF8;\u0026#34; CHANGED=\u0026#34;yes\u0026#34; COMMENT=\u0026#34;$COMMENT Database $DBNAME created.\u0026#34; # if an sql script path has been given, try to execute it on the created db if [[ $# -eq 4 ]]; then zcat \u0026#34;$4\u0026#34; | sudo -u $USER psql -q $DBNAME if [[ $? -eq 0 ]]; then COMMENT=\u0026#34;$COMMENT given SQL script executed\u0026#34; else COMMENT=\u0026#34;$COMMENT failed to execute given SQL script\u0026#34; fi fi fi ExitMessage 0 Some tips I noted about Zabbix:\nThe default system locale must be set for the Zabbix configuration process to work. On the Zabbix server, the agent config file must use the local IP address (server=127.0.0.1). The FQDN does not work (but it works perfectly fine on all other agents\u0026hellip;). Don’t forget to open the following ports: 10050 (server to agents) and 10051 (agents to server). I wanted to automatically add all managed hosts to the server right after agent installations using saltext-zabbix, but at the time of writing, it wasn’t working with the current SaltStack version.\nI had to add all hosts manually\u0026hellip; All in all, Zabbix is probably the least automated part of my homelab. Even the database restore and configuration deployment are manual. I probably need to work on that, but I found Zabbix not to be at a \u0026ldquo;cloud-ready\u0026rdquo; level yet (no \u0026ldquo;as code\u0026rdquo; configuration).\nNtfy as notification solution A good monitoring solution is of limited use without a reliable notification mechanism. This is where ntfy comes into play.\nNtfy is a simple, free way to send and receive notifications on your smartphone. The usage is straightforward: just push a string via HTTP to a dedicated channel, and you can instantly receive the string on your smartphone.\nOf course, the default free and cloud-based approach has limited security protection, but it meets the requirements for a home lab.\nTo pair it with Zabbix, a specific \u0026ldquo;mediatype\u0026rdquo; must be imported. You can find this in the following GitHub repository: torgrimt/zabbix-ntfy: Mediatype to add support for ntfy.sh services.\nHowever, I found that the configuration of Ntfy’s mediatype in Zabbix is not well explained:\nField Comment URL Must contain the URL to the Ntfy server without “http://” or “https://”. For example, “ntfy.sh” is valid, but “https://ntfy.sh” is not valid. Password, Token and Username These fields must be empty if not needed. By “empty,” I mean nothing—remove even the macro reference if not used (even if the macro is empty). Topic Must contain the topic you want to use on the Ntfy server without any “/” character. Backup strategy Because most things are defined \u0026ldquo;as code\u0026rdquo; and based on a desired state approach, my backup needs are lightweight and very targeted: \u0026ldquo;live\u0026rdquo; data.\nI actually have three types of \u0026ldquo;live\u0026rdquo; data to back up:\nUser files, hosted on a NAS (more on that later). Zabbix database: I haven’t yet found an elegant way to back up and restore automatically via states. For now, I do a simple \u0026ldquo;pg_dump\u0026rdquo; from time to time (I\u0026rsquo;m mostly interested in backing up the configuration more than data history). Kubernetes persistent volumes: This is what I will detail below. My goal was to follow a KISS approach to make it easy to recover data in any circumstance. I mean \u0026ldquo;agnostic\u0026rdquo;: even if I had to change the infrastructure logic, hardware, containers, hypervisor, operating system, etc.\nTo achieve this, I based my implementation on ZFS volumes (prefixed by \u0026ldquo;pv_\u0026rdquo; for \u0026ldquo;Persistent Volumes\u0026rdquo;):\nAny \u0026ldquo;pv\u0026rdquo; is a ZFS volume created on all Proxmox nodes and mounted on all Kubernetes nodes. \u0026ldquo;pv\u0026rdquo; volumes are synchronized using \u0026ldquo;pvesync\u0026rdquo; at the hypervisor level. All \u0026ldquo;pv\u0026rdquo; volumes are backed up from node-1 (which has NL disks) by simply mounting them in read-only mode and creating a tar.gz archive. I keep 7 rolling backups for each volume. More details on how these persistent volumes are handled are in the K3s chapter below.\nRegarding the synchronization of ZFS volumes, \u0026ldquo;pvesync\u0026rdquo; works with a master/slave logic. In other words, it doesn’t handle bi-directional synchronization. This can be a problem because Kubernetes pods can freely move from one node to another. So, it’s necessary to determine which host holds the \u0026ldquo;master\u0026rdquo; ZFS volume in order to initiate synchronization accordingly.\nAfter some research, I found that the best way to determine the \u0026ldquo;master\u0026rdquo; volume from the hypervisor was by checking the \u0026ldquo;bytes written\u0026rdquo; property of the ZFS volume. I based the selection of the \u0026ldquo;master\u0026rdquo; volume on the node where the ZFS volume has the highest \u0026ldquo;bytes written\u0026rdquo; value.\nHere is the script. It determines the master node and then launches the synchronization if needed:\nClick to expand code#!/bin/bash source /usr/local/bin/set_common.sh ### GLOBAL VARIABLES ########################### VOLUME_PREFIX=\u0026#34;pv_\u0026#34; declare -A PV ### FUNCTIONS ################################## Help() { # Display Help echo echo \u0026#34;Syncronize all zfs volume that begin with \u0026#39;$VOLUME_PREFIX\u0026#39; with all given hosts\u0026#34; echo \u0026#34;All hosts are scanned and the source of each volume is determined by checking bytes written.\u0026#34; echo echo \u0026#34;The host on which a volume has the highest number of written bytes is considered as the source.\u0026#34; echo \u0026#34;All others hosts will be considered as destinations on which data must be updated\u0026#34; echo \u0026#34;If not change has been made on the volume, no operation is done\u0026#34; echo echo \u0026#34;This command depends on pve-zsync, it must be present and no check is done for that\u0026#34; echo echo \u0026#34;for each found volumes, the output will be:\u0026#34; echo \u0026#34;datetime volume written source_host status\u0026#34;. echo \u0026#34;\u0026#34; echo \u0026#34;examples\u0026#34;. echo \u0026#34;2024-11-09T19:01:02,506680931+01:00 zfs-storage/pv_myvolume 10.0.0.1=674537 / 10.0.0.1=7656 / =\u0026gt; 10.0.0.1 OK\u0026#34;. echo \u0026#34;2024-11-09T19:01:02,506680931+01:00 zfs-storage/pv_myvolume 10.0.0.1=0 / 10.0.0.2=0 no_change OK\u0026#34;. echo echo \u0026#34;status can be: OK, ERROR or SKIPPED (if source is not localhost)\u0026#34; echo echo \u0026#34;Syntax: $0 \u0026lt;host1\u0026gt; \u0026lt;host2\u0026gt; [host3 [...]]\u0026#34; echo } # $1 = zfs volume # $2 = prefix GetZFSSnapshots() { prefix=\u0026#34;$2\u0026#34; $prefix zfs list -H -t snapshot -o name -s creation | \\grep \u0026#34;$1\u0026#34; } ### SCRIPT LOGIC ################################ # Display help if needed if [[ ($# -lt 2) || $1 == \u0026#34;help\u0026#34; ]] ; then Help exit 2 fi # scan all zfs volume used as pv for pv_path in $(zfs list -H | \\grep pv_ | awk \u0026#39;{print $1}\u0026#39;) do echo -n \u0026#34;$(date -Ins) $pv_path \u0026#34; lastWrittenValue=0 sourceHost=\u0026#34;\u0026#34; # get amount of written bytes on all servers and set source host regarding the highest bytes written value for current_host in \u0026#34;$@\u0026#34; do # if not localhost, call command throught ssh prefix=\u0026#34;\u0026#34; if [[ $(hostname --all-ip-addresses || hostname -I) != *\u0026#34;$current_host\u0026#34;* ]]; then prefix=\u0026#34;ssh $current_host\u0026#34; fi # if this current scanned host has more written bytes for this volume, consider it as source written=$($prefix zfs get -H -p written $pv_path | awk \u0026#39;{print $3}\u0026#39;) if [[ $written -gt $lastWrittenValue ]]; then sourceHost=$current_host fi echo -n \u0026#34;$current_host=$written / \u0026#34; lastWrittenValue=$written PV[$current_host]=$written done # if source host has not been determined, it means that no bytes have been written since the previous sync if [[ -z \u0026#34;${sourceHost}\u0026#34; ]]; then echo \u0026#34;=\u0026gt; no_change OK\u0026#34; continue else #echo -n \u0026#34;${PV[$sourceHost]} $sourceHost\u0026#34; echo -n \u0026#34;=\u0026gt; $sourceHost \u0026#34; fi # sync to all sources if sourceHost is localhost if [[ $(hostname --all-ip-addresses || hostname -I) == *\u0026#34;$sourceHost\u0026#34;* ]]; then for current_host in \u0026#34;$@\u0026#34; do if [[ $current_host != $sourceHost ]]; then # Synchronise from source to destination pve-zsync sync --source $pv_path --dest $current_host:$(dirname $pv_path) --maxsnap 5 if [[ $? -eq 0 ]]; then echo \u0026#34; OK\u0026#34; else echo \u0026#34; ERROR\u0026#34; fi fi done else echo \u0026#34; SKIPPED\u0026#34; fi done exit 0 This script runs every 5 minutes on each defined \u0026ldquo;pvhost\u0026rdquo; pillars (see bellow), which is acceptable for me as I have very few write operations on my containers. The probability of losing data because a pod might have been moved just after a write operation but before the next synchronization is very low. Through, I still have the opportunity to go as low as a sync every minute.\nEven though it works very well, such a mechanism must be properly monitored to quickly detect any errors (e.g., network failure). Each call of the script logs the results in a file (/var/log/zfs_sync.log).\nI created a dedicated Zabbix item and trigger to be sure I\u0026rsquo;m alerted by Ntfy in case of error:\nAfter several months in production, I’ve never lost anything and haven’t experienced any filesystem corruption (I use ext4 on top of ZFS volumes). However, I did encounter some errors—not due to the mechanism itself, but because of the USB network adapter dedicated to the replication network on one of the nodes. This adapter is sometimes reset by the kernel.\nLast but not least, persistent volumes are automatically created by states and defined in pillar values. I created another script to handle their creation. This script also takes care of automatically restoring any backup during the creation process. So, again, if I deleted everything, SaltStack will recreate all volumes with their latest available data.\nHere is how I define a persistent volume in pillar:\nClick to expand code############################################################## # Define zfs volumes to be used as persistents volumes in k3s # size is in Gb # scsiid must be unique for each volume ############################################################## pvdef: mariadb: zfsmountpoint: zfs-storage size: 4 fstype: ext4 scsiid: 1 ############################################################## # Define zfs volumes to be used as persistents volumes in k3s # size is in Gb # scsiid must be unique for each volume ############################################################## pvdef: mariadb: zfsmountpoint: zfs-storage size: 4 fstype: ext4 scsiid: 1 mariadbbackup: zfsmountpoint: zfs-storage size: 1 fstype: ext4 scsiid: 5 ############################################################## # list of hosts ip addresses on which all persistents volumes # must be synchronized through pve-zsync using zfs_sync.sh # ipaddr: ip address used for zfs to send / receive sync data # cronsync: cronexpression at which the host execute zfs_sync.sh # # WARNING: be sure that no host launch the sync at the same time ! # ############################################################## pvhosts: hulk-1: ipaddr: 10.0.0.1 cronsync: \u0026#39;0-59/10 * * * *\u0026#39; hulk-2: ipaddr: 10.0.0.2 cronsync: \u0026#39;5-59/10 * * * *\u0026#39; ############################################################## # list of persistants volumes and their Virtual Machine # attachement (ID) ############################################################## pvmap: mariadb: 1200 mariadbbackup: 1200 How states handle the creation:\nClick to expand code# handle k3s persistent volumes creation {% if pillar.get(\u0026#39;pvmap\u0026#39;, none) is not none %} {% for app in pillar[\u0026#39;pvmap\u0026#39;] %} pv_{{ app }}_{{ grains[\u0026#39;host\u0026#39;] }}: cmd.script: - name: /usr/local/bin/set_zfspv.sh - stateful: True - args: \u0026gt; zfsmountpoint={{ pillar[\u0026#39;pvdef\u0026#39;][app][\u0026#39;zfsmountpoint\u0026#39;] }} appname={{ app }} size={{ pillar[\u0026#39;pvdef\u0026#39;][app][\u0026#39;size\u0026#39;] }} fstype={{ pillar[\u0026#39;pvdef\u0026#39;][app][\u0026#39;fstype\u0026#39;] }} vmid={{ pillar[\u0026#39;pvmap\u0026#39;][app] }} scsiid={{ pillar[\u0026#39;pvdef\u0026#39;][app][\u0026#39;scsiid\u0026#39;] }} # try to restore a backup if persistent volume has just been created {% if pillar.get(\u0026#39;has_nas_storage\u0026#39;, false) is true %} pv_{{ app }}_{{ grains[\u0026#39;host\u0026#39;] }}_restore: cmd.run: - names: - /usr/local/bin/nas.sh restore {{ pillar[\u0026#39;pvdef\u0026#39;][app][\u0026#39;zfsmountpoint\u0026#39;] }} pv_{{ app }} - /usr/local/bin/zfs_sync.sh {% for pvhost in pillar[\u0026#39;pvhosts\u0026#39;] %}{{ pvhost }} {% endfor %} \u0026gt;\u0026gt; /var/log/zfs_sync.log - onchanges: - cmd: pv_{{ app }}_{{ grains[\u0026#39;host\u0026#39;] }} {% endif %} {% endfor %} {% endif %} How the states ensure ZFS persistent volumes are synchronized:\nClick to expand code{% if pillar.get(\u0026#39;pvhosts\u0026#39;, none) is not none %} {% set currentlocalhost = grains[\u0026#39;localhost\u0026#39;] %} iac_zfssync_packages: pkg.installed: - pkgs: - pve-zsync iac_zfssync_cmd: file.managed: - names: - /usr/local/bin/zfs_sync.sh: - source: salt://iac_backend/files/zfs_sync.sh - mode: 750 - /etc/logrotate.d/zfs_sync: - source: salt://iac_backend/files/zfs_sync.logrotate - mode: 644 iac_zfssync_cronjob: schedule.present: - function: cmd.run - job_args: - \u0026#39;/usr/local/bin/zfs_sync.sh {% for pvhost in pillar[\u0026#39;pvhosts\u0026#39;] %}{{ pillar[\u0026#39;pvhosts\u0026#39;][pvhost][\u0026#39;ipaddr\u0026#39;] }} {% endfor %} \u0026gt;\u0026gt; /var/log/zfs_sync.log\u0026#39; - maxrunning: 1 - cron: \u0026#39;{{ pillar[\u0026#39;pvhosts\u0026#39;][currentlocalhost][\u0026#39;cronsync\u0026#39;] }}\u0026#39; {% endif %} The script beneath the states (/usr/local/bin/set_zfspv.sh, which is statefull compliant):\nClick to expand code#!/bin/bash source /usr/local/bin/set_common.sh ### GLOBAL VARIABLES ########################### VOLUME_PREFIX=\u0026#34;pv_\u0026#34; MANDATORY=\u0026#34;zfsmountpoint appname size fstype vmid scsiid\u0026#34; ### FUNCTIONS ################################## Help() { # Display Help echo echo \u0026#34;Unsure a ZFS volume for kubernetes PV exists and is provided to guest VM\u0026#34; echo \u0026#34;Either create and provide it, or do nothing\u0026#34; echo echo \u0026#34;Syntax: $0 parameter1=value1 paramter2=value2 ...\u0026#34; echo echo \u0026#34;where valid parameters are (all are mandatory):\u0026#34; echo \u0026#34; zfsmountpoint: root zfs mountpoint on which to create the volume\u0026#34; echo \u0026#34; appname: name of the application in kubernetes (eg. mariadb)\u0026#34; echo \u0026#34; size: size of the volume (eg. 8 for 8 Gigabytes)\u0026#34; echo \u0026#34; fstype: filesystem to use to format the partition (eg. ext4)\u0026#34; echo \u0026#34; vmid: VMID of the VM to which the volume must be added\u0026#34; echo \u0026#34; scsiid: SCSI disk number to use on guest VM (eg. 1 for scsi1) \u0026#34; echo } ### SCRIPT LOGIC ################################ # Display help if needed if [[ ($# -lt 6) || $1 == \u0026#34;help\u0026#34; ]] ; then Help exit 2 fi ParseParameters \u0026#34;$@\u0026#34; CheckMandatory ZFSVolumePath=\u0026#34;${arguments[\u0026#39;zfsmountpoint\u0026#39;]}/${VOLUME_PREFIX}${arguments[\u0026#39;appname\u0026#39;]}\u0026#34; ZFSBlockDevice=\u0026#34;/dev/zvol/$ZFSVolumePath\u0026#34; ZFSBlockDevicePart1=\u0026#34;${ZFSBlockDevice}-part1\u0026#34; MkfsCMD=\u0026#34;mkfs.${arguments[\u0026#39;fstype\u0026#39;]}\u0026#34; ProductName=\u0026#34;${VOLUME_PREFIX}${arguments[\u0026#39;appname\u0026#39;]}\u0026#34; # create volume, partition and format it if not exists if [[ ! -b \u0026#34;$ZFSBlockDevice\u0026#34; ]]; then # Create ZFS volume zfs create -V ${arguments[\u0026#39;size\u0026#39;]}G $ZFSVolumePath if [[ ! $? -eq 0 ]]; then CHANGED=\u0026#34;no\u0026#34; COMMENT=\u0026#34;Error creating zfs volume $ZFSVolumePath\u0026#34; ExitMessage 1 fi # create one partition the volume while [ ! -b \u0026#34;$ZFSBlockDevice\u0026#34; ]; do sleep 1 done parted \u0026#34;$ZFSBlockDevice\u0026#34; -- mklabel msdos parted -a minimal \u0026#34;$ZFSBlockDevice\u0026#34; -s -- mkpart primary 0 -1 if [[ ! $? -eq 0 ]]; then CHANGED=\u0026#34;no\u0026#34; COMMENT=\u0026#34;Error creating partition on zfs volume ${VOLUME_PREFIX}${arguments[\u0026#39;appname\u0026#39;]}\u0026#34; ExitMessage 1 fi # format the new partition when it appears while [ ! -b \u0026#34;$ZFSBlockDevicePart1\u0026#34; ]; do sleep 1 done sleep 2 # still need to wait a bit for the part to settle $MkfsCMD \u0026#34;$ZFSBlockDevicePart1\u0026#34; if [[ ! $? -eq 0 ]]; then CHANGED=\u0026#34;no\u0026#34; COMMENT=\u0026#34;Error with $MkfsCMD $ZFSBlockDevicePart1\u0026#34; ExitMessage 1 fi CHANGED=\u0026#34;yes\u0026#34; COMMENT=\u0026#34;$ZFSVolumePath created.\u0026#34; else CHANGED=\u0026#34;no\u0026#34; COMMENT=\u0026#34;$ZFSVolumePath already exists.\u0026#34; fi # add volume as disk to the guest VM if [[ \u0026#34;$(qm config ${arguments[\u0026#39;vmid\u0026#39;]} | grep ${arguments[\u0026#39;appname\u0026#39;]})\u0026#34; != *\u0026#34;${arguments[\u0026#39;appname\u0026#39;]},\u0026#34;* ]]; then qm set ${arguments[\u0026#39;vmid\u0026#39;]} --scsi${arguments[\u0026#39;scsiid\u0026#39;]} $ZFSBlockDevice,ssd=1,product=\u0026#34;$ProductName\u0026#34; if [[ ! $? -eq 0 ]]; then CHANGED=\u0026#34;no\u0026#34; COMMENT=\u0026#34;Error adding $ZFSBlockDevice as scsi${arguments[\u0026#39;scsiid\u0026#39;]} on VM ${arguments[\u0026#39;vmid\u0026#39;]}\u0026#34; ExitMessage 1 fi CHANGED=\u0026#34;yes\u0026#34; COMMENT=\u0026#34;$COMMENT Disk scsi${arguments[\u0026#39;scsiid\u0026#39;]} added to VM ${arguments[\u0026#39;vmid\u0026#39;]}\u0026#34; else CHANGED=\u0026#34;no\u0026#34; COMMENT=\u0026#34;$COMMENT Guest ${arguments[\u0026#39;vmid\u0026#39;]} already use it\u0026#34; fi # print saltstack readble message then exit with success ExitMessage 0 You can also see a call to a script named \u0026ldquo;/usr/local/bin/nas.sh\u0026rdquo;, it will be detailled later.\nAutomatic updates As of writing, all my LXC and virtual machines are based on Debian 12. Thus I deployed unnattended upgrade on all of hosts via a simple state, based on the official documentation:\nautoupdate_packages_debian: pkg.installed: - pkgs: - unattended-upgrades - apt-listchanges /etc/apt/apt.conf.d/52unattended-upgrades-local: file.managed: - source: salt://sysadmin/files/52unattended-upgrades-local - mode: 640 /etc/apt/apt.conf.d/02periodic: file.managed: - source: salt://sysadmin/files/apt_conf_02periodic - mode: 640 However, I choosed not to apply automated upgrade on Proxmox nodes as it may cause serious issues due to breaking changes (eg. change in network cards names).\nReal life use cases NAS As explained previously, one of the nodes (node-1) has two 8-terabyte hard drives. The main purpose is to build a NAS, primarily to offer Samba shares that host music, movies, office files, backups, etc.\nOne disk is \u0026ldquo;live,\u0026rdquo; and the other is used solely as a mirror/backup. The mirror disk is switched off most of the time and is even removed when I leave home for more than two days.\nThe mirroring is handled on the hypervisor side with the help of a custom script. This script also manages ZFS persistent volume backups and restores, and it can generate email reports.\nClick to expand code#!/bin/bash ### GLOBAL VARIABLES ########################### LOG_TMP_FILE=\u0026#34;/tmp/nas_last_sync.log\u0026#34; SYNC_SOURCE=\u0026#34;/nas-storage\u0026#34; SYNC_DEST=\u0026#34;/nas-mirror\u0026#34; BACKUP_DIR=\u0026#34;$SYNC_SOURCE/backup\u0026#34; DATE_FORMAT=\u0026#34;/bin/date +%d.%m.%Y-%H:%M:%S\u0026#34; SUBJECT_SYNC=\u0026#34;NAS Sync result\u0026#34; SUBJECT_REPORT=\u0026#34;NAS storage report\u0026#34; SUBJECT_BACKUP=\u0026#34;NAS zfs pv_* backup\u0026#34; SUBJECT_RESTORE=\u0026#34;NAS zfs pv_* restore\u0026#34; MIRROR_DISK_LABEL=\u0026#34;NAS-MIRROR\u0026#34; VOL_TO_RESTORE=\u0026#34;\u0026#34; ZFS_POOL=\u0026#34;\u0026#34; BACKUP_NB=4 NL=$\u0026#39;\\n\u0026#39; ### FUNCTIONS ################################## Help() { # Display Help echo echo \u0026#34;Perform NAS operations on NL disks\u0026#34; echo echo \u0026#34;Syntax: $0 \u0026lt;command\u0026gt;\u0026#34; echo \u0026#34;where command can be\u0026#34; echo \u0026#34;- sync: launch disks synchdonisation\u0026#34; echo \u0026#34;- report: build usage report and send it\u0026#34; echo \u0026#34;- backup: backup all zfs pv_* volumes to $BACKUP_DIR\u0026#34; echo \u0026#34;- restore \u0026lt;zfs pool\u0026gt; \u0026lt;vol\u0026gt;: restore content of vol from $BACKUP_DIR\u0026#34; echo \u0026#34;- help: display this message\u0026#34; echo } # send a email to sysadmin # $1 = subject # $2 = content (string or file) mailadmin() { if [ -f \u0026#34;$2\u0026#34; ]; then mail -s \u0026#34;$1\u0026#34; root \u0026lt; \u0026#34;$2\u0026#34; else echo \u0026#34;$2\u0026#34; | mail -s \u0026#34;$1\u0026#34; root fi } # umount, send mail then exit with given code # $1 = exit code # $2 = path to umount # $3 = subject # $4 = content (string or file) mailandexit() { if [[ ! -z \u0026#34;$2\u0026#34; ]];then umount -q $2 fi mailadmin \u0026#34;$3\u0026#34; \u0026#34;$4\u0026#34; exit $1 } # snychornize nas-storage to nas-mirror if disk is present Sync() { if [ -e /dev/disk/by-label/$MIRROR_DISK_LABEL ] ; then mount -v $SYNC_DEST \u0026gt; \u0026#34;$LOG_TMP_FILE\u0026#34; 2\u0026gt;\u0026amp;1 echo \u0026#34;---------------------------------------------------------\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_TMP_FILE\u0026#34; rsync -a -v --delete-after $SYNC_SOURCE/ $SYNC_DEST/ | awk \u0026#39;{ print \u0026#34;[\u0026#39;$($DATE_FORMAT)\u0026#39;] \u0026#34; $0 }\u0026#39; \u0026gt;\u0026gt; \u0026#34;$LOG_TMP_FILE\u0026#34; 2\u0026gt;\u0026amp;1 echo \u0026#34;---------------------------------------------------------\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_TMP_FILE\u0026#34; umount -v $SYNC_DEST \u0026gt;\u0026gt; \u0026#34;$LOG_TMP_FILE\u0026#34; 2\u0026gt;\u0026amp;1 mailadmin \u0026#34;$SUBJECT_SYNC\u0026#34; \u0026#34;$LOG_TMP_FILE\u0026#34; fi } # generate a simple usage report of nas storage Report() { message=\u0026#34;$(df -h $SYNC_SOURCE)\u0026#34; message=\u0026#34;$message$NL --------------------------------------------------------------$NL\u0026#34; for dir in $(ls $SYNC_SOURCE) do message=\u0026#34;$message $(du -h -d 1 $SYNC_SOURCE/$dir)$NL$NL\u0026#34; done mailadmin \u0026#34;$SUBJECT_REPORT\u0026#34; \u0026#34;$message\u0026#34; } # backup all zfs pv_* vol backup_zfspv() { message=\u0026#34;ZFS pv_* backup:$NL\u0026#34; message=\u0026#34;$message --------------------------------------------------------------$NL\u0026#34; for pv_path in $(zfs list -H | \\grep pv_ | awk \u0026#39;{print $1}\u0026#39;) do mountpath=\u0026#34;/dev/zvol/${pv_path}-part1\u0026#34; base=\u0026#34;$(basename $pv_path)\u0026#34; filename=\u0026#34;$base-$(date -I).tar.gz\u0026#34; message=\u0026#34;$message $filename \u0026#34; # do nothing if backup destination already exists if [[ -f \u0026#34;$BACKUP_DIR/$filename\u0026#34; ]]; then message=\u0026#34;$message INFO: file already exists $NL\u0026#34; continue fi # mount the source to backup mkdir -p /media/$pv_path mount -o ro $mountpath /media/$pv_path if [[ ! $? -eq 0 ]]; then message=\u0026#34;$message ERROR: could not mount $mountpath $NL\u0026#34; continue fi # if volume is empty, do nothing if [[ $(ls /media/$pv_path | wc -l) -lt 2 ]] then message=\u0026#34;$message INFO: empty source, nothing done $NL\u0026#34; umount /media/$pv_path continue fi # do the backup cd /media/$pv_path tar -czf \u0026#34;$BACKUP_DIR/$filename\u0026#34; * if [[ ! $? -eq 0 ]]; then message=\u0026#34;$message ERROR: creating tar.gz archive on $BACKUP_DIR $NL\u0026#34; umount /media/$pv_path continue fi # report size message=\u0026#34;$message$(du -h \u0026#34;$BACKUP_DIR/$filename\u0026#34; | awk \u0026#39;{print $1}\u0026#39;) $NL\u0026#34; cd .. umount /media/$pv_path if [[ $? -eq 0 ]]; then rmdir /media/$pv_path fi # if there are more backup files than BACKUP_NB, delete the oldest if [[ $(ls $BACKUP_DIR/$base* | wc -l) -gt $BACKUP_NB ]] then file_to_delete=$(ls $BACKUP_DIR/$base* | sort | head -1) rm $file_to_delete fi done mailadmin \u0026#34;$SUBJECT_BACKUP\u0026#34; \u0026#34;$message\u0026#34; } # restore a given zfs volume restore_zfspv() { if [[ -z \u0026#34;$VOL_TO_RESTORE\u0026#34; || -z \u0026#34;$ZFS_POOL\u0026#34; ]]; then Help exit 0 fi message=\u0026#34;Restore $VOL_TO_RESTORE:$NL\u0026#34; message=\u0026#34;$message --------------------------------------------------------------$NL\u0026#34; # get lastest backup if any file_to_restore=$(ls $BACKUP_DIR/$VOL_TO_RESTORE* 2\u0026gt;/dev/null | sort -r | head -1) if [[ ! -f $file_to_restore ]]; then message=\u0026#34;$message INFO: no backup found in $BACKUP_DIR\u0026#34; mailandexit 0 \u0026#34;\u0026#34; \u0026#34;$SUBJECT_RESTORE\u0026#34; \u0026#34;$message\u0026#34; fi # mount vol to restore mountpath=\u0026#34;/dev/zvol/${ZFS_POOL}/${VOL_TO_RESTORE}-part1\u0026#34; mkdir -p /media/$VOL_TO_RESTORE mount $mountpath /media/$VOL_TO_RESTORE if [[ ! $? -eq 0 ]]; then message=\u0026#34;$message ERROR: could not mount $mountpath\u0026#34; mailandexit 1 /media/$VOL_TO_RESTORE \u0026#34;$SUBJECT_RESTORE\u0026#34; \u0026#34;$message\u0026#34; fi # if volume not empty, do nothing if [[ $(ls /media/$VOL_TO_RESTORE | wc -l) -gt 2 ]] then message=\u0026#34;$message WARN: target not empty, nothing done $NL\u0026#34; mailandexit 0 /media/$VOL_TO_RESTORE \u0026#34;$SUBJECT_RESTORE\u0026#34; \u0026#34;$message\u0026#34; fi # restore message=\u0026#34;$message $(tar -zxvf $file_to_restore -C /media/$VOL_TO_RESTORE/)$NL$NL\u0026#34; if [[ ! $? -eq 0 ]]; then message=\u0026#34;$message ERROR: could not restore $file_to_restore\u0026#34; mailandexit 1 /media/$VOL_TO_RESTORE \u0026#34;$SUBJECT_RESTORE\u0026#34; \u0026#34;$message\u0026#34; fi # umount umount /media/$VOL_TO_RESTORE if [[ $? -eq 0 ]]; then rmdir /media/$VOL_TO_RESTORE fi message=\u0026#34;$message restored $file_to_restore into $mountpath $NL\u0026#34; mailadmin \u0026#34;$SUBJECT_RESTORE\u0026#34; \u0026#34;$message\u0026#34; } ### SCRIPT LOGIC ################################ case \u0026#34;$1\u0026#34; in sync) Sync ;; report) Report ;; backup) backup_zfspv ;; restore) ZFS_POOL=\u0026#34;$2\u0026#34; VOL_TO_RESTORE=\u0026#34;$3\u0026#34; restore_zfspv ;; *) Help ;; esac In additions, most important files are also synchronized on Onedrive which is monitored via a custom log item plus a trigger in Zabbix for the file \u0026ldquo;/var/log/onedrive\u0026rdquo;\nThe NAS service itself is provided by a dedicated LXC on node-1. This LXC uses the \u0026ldquo;online\u0026rdquo; hard disk to provides SMB shares on a VLAN dedicated to users. Users confguration has been covered in the previous chapter \u0026ldquo;Users management\u0026rdquo;.\nThe state that configure the LXC is pretty simple:\n# source : https://wiki.archlinux.fr/Samba + https://wiki.debian.org/Avahi # install samba filesrv_packages: pkg.installed: - pkgs: - samba - avahi-daemon - wsdd # custom configuration file /etc/samba/smb.conf: file.managed: - source: salt://services/files/smb.conf - user: root - group: root - mode: 644 # ensure service is active and running smbd: service.running: - enable: True - watch: - file: /etc/samba/smb.conf nmbd: service.running: - enable: True - watch: - file: /etc/samba/smb.conf wsdd: service.running: - enable: True avahi-daemon: service.running: - enable: True Reverse proxy and WAF As explained later, I have a Kubernetes cluster on which some exposed services are running. These services are behind an Nginx server that serves as a reverse proxy, SSL offloading, and a web application firewall thanks to NAXSI.\nNginx is fully deployed on a dedicated LXC running on node-2 via a couple of states.\nBelow is an example of pillar values to set up Nginx. Crypto keys are encrypted with GPG:\nClick to expand code#!jinja|yaml|gpg ############################################################## # reverse proxy configuration with in-memory cache # secrets are encrypted with GPG # # rproxy_conf : # - ssl_certificate and ssl_certificate_key are GPG encrypted data for let\u0026#39;s encrypt. Wildcard certificate is assumed (same for all sites) # # rproxy_sites: # - type: used for specific settings and naxsi rules file name naxsi_\u0026lt;type\u0026gt;.rules # ############################################################## rproxy_conf: naxsi_package: https://github.com/wargio/naxsi/releases/download/1.6/debian-bookworm-libnginx-mod-http-naxsi_1.6_amd64.deb worker_connections: 512 # maximum number of simultaneous connections that can be opened by a worker process resolver: aaa.bbb.ccc.ddd # ip address of dns server to use to resolve backend fqdn cache_max_age: 14d # delete file older than x days cache_max_size: 400m # maximum size the cache can take () dh4096: | -----BEGIN PGP MESSAGE----- [...] -----END PGP MESSAGE----- ssl_certificate: | -----BEGIN PGP MESSAGE----- [...] -----END PGP MESSAGE----- ssl_certificate_key: | -----BEGIN PGP MESSAGE----- [...] -----END PGP MESSAGE----- rproxy_certbot: rsa-key-size: 4096 cert-name: all-sites email: registered@email.com domains: www.mydomain.com,www.myother-domain.org deploy-hook: systemctl restart nginx rproxy_sites: mydomain.com: type: wordpress cache_static_files: enabled cache_dynamic_files: enabled backends: - k3s-master-1.local.lan:8001 - k3s-master-2.local.lan:8001 myother-domain.org: type: wordpress cache_static_files: enabled cache_dynamic_files: enabled backends: - k3s-master-1.local.lan:8002 - k3s-master-2.local.lan:8002 And here it the state file:\nClick to expand code############################################################## # Deploy everything that is required to run nginx as a reverseproxy + waf + cache: # - needed packages # - external deb package for naxsi extention # - custom config for nginx service in order to create /run/nginx directory for tmpfs caching # - install naxsi rules files and cache configurations # - make custom nginx config file regarding pillar values # - set let\u0026#39;s encrypt required certiitates file based on pillar GPG encrypted values # - create site configuration file for each site declared in pillar # - set a cronjob that runs certbot everyday to try to renew the wildcards certificate ############################################################## # install packages rproxy_packages: pkg.installed: - pkgs: - nginx - certbot - python3-certbot-nginx # install naxsi extention, assume debian host rproxy_sources_packages: cmd.run: - name: wget {{ pillar[\u0026#39;rproxy_conf\u0026#39;][\u0026#39;naxsi_package\u0026#39;] }} -O /root/naxsi.deb \u0026amp;\u0026amp; dpkg -i /root/naxsi.deb \u0026amp;\u0026amp; rm /root/naxsi.deb - unless: test -f /usr/lib/nginx/modules/ngx_http_naxsi_module.so # tune systemd nginx service config rproxy_systemd_config: file.managed: - name: /etc/systemd/system/nginx.service.d/custom-service.conf - source: salt://services/files/rproxy/custom-service.conf - makedirs: True # install available naxsi rules rproxy_naxsi_rules: file.recurse: - name: /etc/nginx/naxsi-rules - source: salt://services/files/rproxy/naxsi-rules # install available reverse proxy cache configurations rproxy_cache_conf: file.recurse: - name: /etc/nginx/reverse-conf - source: salt://services/files/rproxy/reverse-conf # deploy nginx configuration rproxy_conf: file.managed: - name: /etc/nginx/nginx.conf - source: salt://services/files/rproxy/nginx.conf - template: jinja - context: worker_connections: {{ pillar[\u0026#39;rproxy_conf\u0026#39;][\u0026#39;worker_connections\u0026#39;] }} resolver: {{ pillar[\u0026#39;rproxy_conf\u0026#39;][\u0026#39;resolver\u0026#39;] }} cache_max_age: {{ pillar[\u0026#39;rproxy_conf\u0026#39;][\u0026#39;cache_max_age\u0026#39;] }} cache_max_size: {{ pillar[\u0026#39;rproxy_conf\u0026#39;][\u0026#39;cache_max_size\u0026#39;] }} # let\u0026#39;s encrypte cert files rproxy_dh4096: file.managed: - name: /etc/nginx/dh4096.pem - contents: |- {{ pillar[\u0026#39;rproxy_conf\u0026#39;][\u0026#39;dh4096\u0026#39;] | indent(8) }} rproxy_fullchain: file.managed: - name: /etc/letsencrypt/live/all-sites/fullchain.pem - makedirs: True - contents: |- {{ pillar[\u0026#39;rproxy_conf\u0026#39;][\u0026#39;ssl_certificate\u0026#39;] | indent(8) }} rproxy_privkey: file.managed: - name: /etc/letsencrypt/live/all-sites/privkey.pem - makedirs: True - contents: |- {{ pillar[\u0026#39;rproxy_conf\u0026#39;][\u0026#39;ssl_certificate_key\u0026#39;] | indent(8) }} # generate config files for each declared site {% for site in pillar[\u0026#39;rproxy_sites\u0026#39;] %} rproxy_site_{{ site }}: file.managed: - name: /etc/nginx/sites-enabled/{{ site }}.conf - source: salt://services/files/rproxy/sites-enabled.conf - makedirs: True - template: jinja - context: site: {{ site }} {% endfor %} # ensure nginx is running rproxy_service: service.running: - name: nginx - enable: True - restart: True - watch: - file: /etc/nginx/nginx.conf - file: /etc/nginx/sites-enabled/* # auto renew let\u0026#39;s encrypt certificate rproxy_certbot: schedule.present: - function: cmd.run - job_args: - \u0026#39;certbot --nginx --agree-tos --hsts --staple-ocsp --rsa-key-size {{ pillar[\u0026#39;rproxy_certbot\u0026#39;][\u0026#39;rsa-key-size\u0026#39;] }} --cert-name {{ pillar[\u0026#39;rproxy_certbot\u0026#39;][\u0026#39;cert-name\u0026#39;] }} --email {{ pillar[\u0026#39;rproxy_certbot\u0026#39;][\u0026#39;email\u0026#39;] }} --domains {{ pillar[\u0026#39;rproxy_certbot\u0026#39;][\u0026#39;domains\u0026#39;] }}\u0026#39; - maxrunning: 1 - cron: \u0026#39;0 5 * * *\u0026#39; I\u0026rsquo;m not going to share all my nginx configuration files, but as you can see I use a generic \u0026ldquo;site-enabled.conf\u0026rdquo; which is a jinja template.\nk3s cluster Create the cluster If you’ve managed to read the previous chapters and reached this line, you probably already know how my K3S cluster is deployed and configured: via states and pillar values.\nNode-1 and node-2 are primarily used to host services that require high performance or persistence. Node-3 can handle smaller workloads but, more importantly, serves as a third Etcd instance. While it’s not mandatory with K3s, it provides additional security.\nBelow is a compilation of all the pillar values needed to configure the cluster. Of course, they must be distributed across several files to assign values to specific hosts (e.g., first master, simple node, etc.).\nPillar values :\nClick to expand code############################################################## # k3s common settings for all nodes ofthe cluster # url: base url where to get k3s binaries from # test: file to check to choose weither k3s install should be done # tocken: gpg encrypted needed token to join the cluster # echo -n \u0026#39;value to encrypt\u0026#39; | gpg --homedir /etc/salt/gpgkeys --trust-model always -ear unique_ear_key ############################################################## k3s-common: url: https://get.k3s.io test: test -x /usr/local/bin/k3s token: | -----BEGIN PGP MESSAGE----- [...] -----END PGP MESSAGE----- ############################################################## # k3s first master of the cluster. # A server (not lxc) must exists on local.lan # # Any other node of the cluster will first have to connect to this one # Install: command to run in order to deploy k3s on this master node ############################################################## k3s-master: install: sh -s - server --cluster-init --write-kubeconfig-mode=644 --tls-san k3s-master-1.local.lan ############################################################## # k3s master definition for all nodes. # # master: fqdn of the first k3s master ############################################################## k3s-node: master: k3s-master-1.local.lan The following states take care of installing either a primary master or a node, depending on available pillar values:\nClick to expand code############################################################## # Deploy k3s cluster regarding k3s-* pillars. # - The role of the node is defined by its name (k3s-agent-*, k3s-master-*, k3s-server-*) # - The 1st master will be used to add all other nodes # - A custom udev rule is added to all nodes that can handle persistent volumes: # Any scsi disk with a product name that starts with \u0026#34;pv_\u0026#34; is # mounted as /media/pv_\u0026lt;name\u0026gt; on all PV enabled hosts # ############################################################## {% if grains[\u0026#39;host\u0026#39;].startswith(\u0026#39;k3s-agent\u0026#39;) %} {% set role=\u0026#39;agent\u0026#39; %} {% elif pillar.get(\u0026#39;k3s-master\u0026#39;, none) is not none %} {% set role=\u0026#39;\u0026#39; %} {% else %} {% set role=\u0026#39;server\u0026#39; %} {% endif %} k3s_dependencies: pkg.installed: - pkgs: - ca-certificates {% if pillar.get(\u0026#39;k3s-master\u0026#39;, none) is not none %} k3s_install_{{ grains[\u0026#39;host\u0026#39;] }}: cmd.run: - name: wget -q -O - {{ pillar[\u0026#39;k3s-common\u0026#39;][\u0026#39;url\u0026#39;] }} | {{ pillar[\u0026#39;k3s-master\u0026#39;][\u0026#39;install\u0026#39;] }} {% for label in pillar[\u0026#39;k3s-labels\u0026#39;] %}{{ \u0026#39; --node-label \u0026#39; + label + \u0026#39;=\u0026#39; ~ pillar[\u0026#39;k3s-labels\u0026#39;][label] }} {% endfor %} --token {{ pillar[\u0026#39;k3s-common\u0026#39;][\u0026#39;token\u0026#39;] }} - unless: {{ pillar[\u0026#39;k3s-common\u0026#39;][\u0026#39;test\u0026#39;] }} - require: - pkg: k3s_dependencies {% endif %} {% if pillar.get(\u0026#39;k3s-node\u0026#39;, none) is not none %} k3s_install_{{ grains[\u0026#39;host\u0026#39;] }}: cmd.run: - name: wget -q -O - {{ pillar[\u0026#39;k3s-common\u0026#39;][\u0026#39;url\u0026#39;] }} | K3S_URL=\u0026#34;https://{{ pillar[\u0026#39;k3s-node\u0026#39;][\u0026#39;master\u0026#39;] }}:6443\u0026#34; K3S_TOKEN=\u0026#34;{{ pillar[\u0026#39;k3s-common\u0026#39;][\u0026#39;token\u0026#39;] }}\u0026#34; sh -s - {{ role }} {% for label in pillar[\u0026#39;k3s-labels\u0026#39;] %}{{ \u0026#39; --node-label \u0026#39; + label + \u0026#39;=\u0026#39; ~ pillar[\u0026#39;k3s-labels\u0026#39;][label] }} {% endfor %} - unless: {{ pillar[\u0026#39;k3s-common\u0026#39;][\u0026#39;test\u0026#39;] }} - require: - pkg: k3s_dependencies {% endif %} {% if pillar[\u0026#39;k3s-labels\u0026#39;][\u0026#39;pv\u0026#39;] == \u0026#39;enabled\u0026#39; %} k3s_install_udev: file.managed: - names: - /etc/udev/rules.d/80-pv-disks.rules: - source: salt://services/files/80-pv-disks.rules - mode: 640 cmd.run: - name: udevadm control --reload-rules \u0026amp;\u0026amp; udevadm trigger - onchanges: - file: /etc/udev/rules.d/80-pv-disks.rules {% endif %} k3s_running: service.running: {% if role == \u0026#39;agent\u0026#39; -%} - name: k3s-agent {% else -%} - name: k3s {% endif -%} - enable: True - require: - cmd: k3s_install_{{ grains[\u0026#39;host\u0026#39;] }} As you can see, a special udev rule file (80-pv-disks.rules) is installed on all K3S nodes with the pillar value \u0026ldquo;k3s-label:pv\u0026rdquo;. More on than on the next chapter.\nManage persistence I’ve already touched on the subject of persistent volumes in the \u0026ldquo;Backup and Restore\u0026rdquo; chapter. The concept is to use ZFS volumes that are mirrored between node-1 and node-2.\nThese ZFS volumes are mounted as SCSI devices on K3s hosts. They are then automatically mounted and unmounted via udev rules (80-pv-disks.rules). The purpose of \u0026ldquo;automount\u0026rdquo; is to prevent the same volume from being mounted simultaneously on two different nodes, thereby avoiding filesystem corruption. The auto-unmount timeout is set to a relatively short value (5 seconds).\nNow, you might ask: where/how should these SCSI volumes be mounted so that K3s pods can recognize their names? The trick is to use the SCSI property \u0026ldquo;product,\u0026rdquo; which you may have already seen in the script set_zfspv.sh.\nFor example, I have a ZFS volume named pv_mariadb on the hypervisor side. This volume is presented as a SCSI device on all nodes with a command like:\nqm set 1200 --scsi0 /dev/zvol/zfs-storage/pv_mariadb-part1,ssd=1,product=\u0026quot;pv_mariadb\u0026quot;\nThen, on all hosts with the pillar value k3s-label:pv , there is a set of udev rules that automatically mount all SCSI devices and create mount points according to the SCSI property \u0026ldquo;product\u0026rdquo; (exposed as \u0026ldquo;ID_MODEL\u0026rdquo; by udev). For example, /media/pv_mariadb. This way, I can define a pod that references a local storage path, and no matter where the pod is deployed, the data will be accessible.\nBelow are the udev rules :\n# create mount dir ACTION==\u0026#34;add\u0026#34;, KERNEL==\u0026#34;sd[a-z]?\u0026#34;, ENV{ID_MODEL}==\u0026#34;pv_*\u0026#34;, RUN+=\u0026#34;/bin/mkdir -p /media/%E{ID_MODEL}\u0026#34; KERNEL==\u0026#34;sd[a-z]?\u0026#34;, ENV{ID_MODEL}==\u0026#34;pv_*\u0026#34;, RUN+=\u0026#34;/bin/mkdir -p /media/%E{ID_MODEL}\u0026#34; # set automount: all kube pv with reside in /media/pv_xxx ACTION==\u0026#34;add\u0026#34;, KERNEL==\u0026#34;sd[a-z]?\u0026#34;, ENV{ID_MODEL}==\u0026#34;pv_*\u0026#34;, RUN+=\u0026#34;/usr/bin/systemd-mount --no-block --automount=yes --timeout-idle-sec=5s --collect $devnode /media/%E{ID_MODEL}\u0026#34; KERNEL==\u0026#34;sd[a-z]?\u0026#34;, ENV{ID_MODEL}==\u0026#34;pv_*\u0026#34;, RUN+=\u0026#34;/usr/bin/systemd-mount --no-block --automount=yes --timeout-idle-sec=5s --collect $devnode /media/%E{ID_MODEL}\u0026#34; # if device is removed, umount it... ACTION==\u0026#34;remove\u0026#34;, KERNEL==\u0026#34;sd[a-z]?\u0026#34;, ENV{ID_MODEL}==\u0026#34;pv_*\u0026#34;, RUN+=\u0026#34;/usr/bin/systemd-mount -u /media/%E{ID_MODEL}\u0026#34; # ... then remove mount dir ACTION==\u0026#34;remove\u0026#34;, KERNEL==\u0026#34;sd[a-z]?\u0026#34;, ENV{ID_MODEL}==\u0026#34;pv_*\u0026#34;, RUN+=\u0026#34;/bin/rmdir /media/%E{ID_MODEL}\u0026#34; And here is a example of a persistent volume definition for K3S (additional nodeAffinity may be required):\napiVersion: v1 kind: PersistentVolume metadata: name: mariadb-data-pv namespace: prod spec: capacity: storage: 3500M volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage claimRef: name: mariadb-data-pv-claim namespace: prod local: path: /media/pv_mariadb By combining this mechanism with the backup and desired-state approach I explained earlier, I can delete any SCSI volume (e.g., qm set 1200 --delete scsi0) or even destroy the ZFS volume (e.g., zfs destroy zfs-storage/pv_mariadb). Everything will be automatically reconstructed and restored, either by manually running salt '*' state.apply or by waiting a bit, as the same command is automatically executed on a regular basis.\nApplication deployment Deploying applications in Kubernetes \u0026ldquo;as code\u0026rdquo; is done with YAML files that include various definitions (volumes, services, containers, deployments, etc.). To maintain a GitOps approach, all applications deployed in my K3s cluster are based on pillar values and states.\nThe concept is that all Kubernetes definition YAML files are managed by SaltStack, which is also responsible for running kubectl apply. By \u0026ldquo;definition files,\u0026rdquo; I mean not only application deployments but also namespaces, secrets, and custom images. As mentioned earlier, any push of a new or modified file to the Git repository triggers a \u0026ldquo;highstate apply.\u0026rdquo; Thus, any newly added YAML file is automatically applied to the K3s cluster.\nHowever, removing a pod is not (yet?) handled automatically. I haven’t implemented that for now, but it shouldn’t be complicated (pillar value + dedicated state that could trigger a kubectl delete).\nHere is an exemple of pillar values I can use:\nClick to expand code#!yaml|gpg ############################################################## # List of applications that are deployed on kubernetes cluster # The name must correspond to a kube yaml definitions file on # salt/kubeapps/files. # # yaml files are managed on all k3s server nodes in /root # # e.g: \u0026#34;mariadb\u0026#34; will trigger kubectl apply -f /root/mariadb.yaml # # kubesecrets part is used to create secrets in k3s cluster # secrets are base64 encoded then encrypted with gpgp # echo -n \u0026#39;value to encrypt\u0026#39; | base64 | gpg --homedir /etc/salt/gpgkeys --trust-model always -ear unique_ear_key ############################################################## kubeapps: - mariadb - mariadbbackup kubesecrets: prod: mariadbpwd: | -----BEGIN PGP MESSAGE----- [...] -----END PGP MESSAGE----- ############################################################## # k3s docker images to install on all nodes # # expected format of docker image name: \u0026lt;name\u0026gt;-\u0026lt;version\u0026gt; # # install: list of image to install # remove: list of image to delete (the \u0026#39;-\u0026#39; is replaced by \u0026#39;:\u0026#39; when trying to remove) # prefix is used to remove images, adding this value to the image name:version ############################################################## k3s-images: prefix: docker.io/library/ install: wordpress-v2024.11.17: https://github.com/jit06/docker-images/releases/download/wordpress/wordpress-v2024.11.17.tar.gz remove: - wordpress-v2024.11.16 - wordpress-latest And below the states that manage namespaces, custom images installation, secrets and applications deployments:\nClick to expand code############################################################## # installation and removall of docker images to local repository # image to install is expected to be a gziped tar archive ############################################################## # install {% for image in pillar[\u0026#39;k3s-images\u0026#39;][\u0026#39;install\u0026#39;] %} {{ image }}_install: cmd.run: - name: wget -q -O - {{ pillar[\u0026#39;k3s-images\u0026#39;][\u0026#39;install\u0026#39;][image] }} | gzip -d - | ctr image import - - unless: test -n \u0026#34;$(ctr images ls | grep {{ image.replace(\u0026#39;-\u0026#39;,\u0026#39;:\u0026#39;) }})\u0026#34; {% endfor %} # remove {% for image in pillar[\u0026#39;k3s-images\u0026#39;][\u0026#39;remove\u0026#39;] %} {{ image }}_remove: cmd.run: - name: ctr images del {{ pillar[\u0026#39;k3s-images\u0026#39;][\u0026#39;prefix\u0026#39;] }}{{ image.replace(\u0026#39;-\u0026#39;,\u0026#39;:\u0026#39;) }} - unless: test -z \u0026#34;$(ctr images ls | grep {{ image.replace(\u0026#39;-\u0026#39;,\u0026#39;:\u0026#39;) }})\u0026#34; {% endfor %} ############################################################## # Handle installation and deployment of apps in k3s cluster # This state should only be applied on a k3s master. # # The list of apps to deploy are checked in \u0026#39;kubeapps\u0026#39; pillar # for each apps: # - the yaml definition from files/\u0026lt;appname\u0026gt;.yaml is copied to /root # - the yaml file is applied via kubectl unless app is already installed ############################################################## # create / refresh namespaces kubeapps_namespaces: file.managed: - name: /root/namespaces.yaml - source: salt://kubeapps/files/namespaces.yaml cmd.run: - name: kubectl apply -f /root/namespaces.yaml # create / refresh secrets than remove the file kubeapps_secrets: file.managed: - name: /root/secrets.yaml - source: salt://kubeapps/files/secrets.yaml - template: jinja cmd.run: - name: kubectl apply -f /root/secrets.yaml # deploy kubapps if not running {% for app in pillar[\u0026#39;kubeapps\u0026#39;] %} {{ app }}_{{ grains[\u0026#39;host\u0026#39;] }}: file.managed: - name: /root/{{ app }}.yaml - source: salt://kubeapps/files/{{ app }}.yaml - template: jinja - mode: 600 apply_{{ app }}_{{ grains[\u0026#39;host\u0026#39;] }}: cmd.run: - name: kubectl apply -f /root/{{ app }}.yaml - unless: test -n \u0026#34;$(kubectl get pods | grep {{ app }})\u0026#34; {% endfor %} Finally, here is an example of my MariaDB definition to be deployed on the cluster, which also embed phpMyAdmin (yes, they shoud be on separates pods\u0026hellip;):\nClick to expand code####################################### # Services ####################################### --- apiVersion: v1 kind: Service metadata: name: mariadb-internal-service namespace: prod spec: selector: app: mariadb ports: - protocol: TCP port: 3306 targetPort: 3306 --- apiVersion: v1 kind: Service metadata: name: phpmyadmin-service namespace: prod spec: selector: app: mariadb type: LoadBalancer ports: - protocol: TCP port: 80 # exposed port for targetPort: 8080 # targeting port on the container # nodePort: 30100 # external IP port from 30000 till 32767 range ####################################### # routes ####################################### --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: phpmyadmin namespace: prod spec: rules: - host: phpmyadmin.local.lan http: paths: - path: / pathType: Prefix backend: service: name: phpmyadmin-service port: number: 80 ####################################### # Maps ####################################### --- apiVersion: v1 kind: ConfigMap metadata: name: mariadb-configmap namespace: prod data: mariadb.cnf: | [mariadb] key_buffer_size=4M max_allowed_packet=16M table_open_cache=64 sort_buffer_size=512K net_buffer_length=8K read_buffer_size=256K read_rnd_buffer_size=512K myisam_sort_buffer_size=8M innodb_buffer_pool_size=64M query_cache_limit=1M query_cache_size=16M tmp_table_size=64M max_heap_table_size=64M max_connections=50 log-warnings slow-query-log general-log disable-log-bin ####################################### # volumes ####################################### --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mariadb-data-pv-claim namespace: prod labels: app: mariadb spec: accessModes: - ReadWriteOnce resources: requests: storage: 3500M storageClassName: local-storage volumeName: mariadb-data-pv --- apiVersion: v1 kind: PersistentVolume metadata: name: mariadb-data-pv namespace: prod spec: capacity: storage: 3500M volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage claimRef: name: mariadb-data-pv-claim namespace: prod local: path: /media/pv_mariadb nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - k3s-master-1 - k3s-master-2 ####################################### # containers ####################################### --- apiVersion: apps/v1 kind: Deployment metadata: name: mariadb-deployment namespace: prod spec: # specification for deployment resource replicas: 1 # how many replicas of pods we want to create selector: matchLabels: app: mariadb template: metadata: namespace: prod labels: app: mariadb # service will look for this label spec: containers: - name: mariadb-internal-service image: mariadb ports: - containerPort: 3306 env: - name: MARIADB_ROOT_PASSWORD valueFrom: secretKeyRef: name: kubesecrets key: mariadbpwd volumeMounts: - name: mariadb-config mountPath: /etc/mysql/conf.d/ # directory will be cleaned at the beginning - name: mariadb-data-pv mountPath: /var/lib/mysql - name: phpmyadmin image: bitnami/phpmyadmin:latest ports: - containerPort: 8080 env: - name: DATABASE_HOST value: mariadb-internal-service volumes: - name: mariadb-config configMap: name: mariadb-configmap defaultMode: 0644 items: - key: mariadb.cnf path: plugin-configuration.cnf - name: mariadb-data-pv persistentVolumeClaim: claimName: mariadb-data-pv-claim affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: pv operator: In values: - enabled requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: speed operator: In values: - fast A Last Word on Secrets Management. You may have seen how I manage secrets:\nSecrets are encoded in base64, as required by K3s. Base64-encoded secrets are then encrypted with GPG and stored in pillar values (which reside in my private Git repository). Secrets are injected into K3s via a temporary YAML file located in /root. This file is always removed after usage, as it contains decrypted secrets. Kubernetes applications If you’ve read this far, you probably understand the concept of my homelab. However, I’ll provide some application deployment examples in this chapter to better illustrate real-life use cases.\nMariadb + phpMyAdmin This deployment was discussed in the previous chapter. However, I use a specific deployment to back up MariaDB. Even though all persistent volumes are replicated and backed up automatically, I find it safer to keep basic, text-based SQL exports of all databases.\nTo do this, I use a Kubernetes cronjob to execute the mariadb-dump command and store SQL files on a dedicated persistent volume.\nClick to expand code####################################### # volumes ####################################### --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mariadb-backup-pv-claim namespace: prod spec: accessModes: - ReadWriteOnce resources: requests: storage: 900M storageClassName: local-storage volumeName: mariadb-backup-pv --- apiVersion: v1 kind: PersistentVolume metadata: name: mariadb-backup-pv namespace: prod spec: capacity: storage: 900M volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage claimRef: name: mariadb-backup-pv-claim namespace: prod local: path: /media/pv_mariadbbackup nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - k3s-master-1 - k3s-master-2 ####################################### # cron job ####################################### --- apiVersion: batch/v1 kind: CronJob metadata: name: mariadb-backup-cronjob namespace: prod spec: schedule: \u0026#34;0 4 * * *\u0026#34; jobTemplate: spec: template: spec: containers: - name: mariadb-backup image: mariadb command: - \u0026#34;/bin/sh\u0026#34; - \u0026#34;-c\u0026#34; - \u0026#39;mariadb -h mariadb-internal-service -u root -p$(cat /secrets/mariadbpwd) -N -e \u0026#34;show databases\u0026#34; | while read dbname; do mariadb-dump -h mariadb-internal-service -u root -p$(cat /secrets/mariadbpwd) --add-drop-database --single-transaction \u0026#34;$dbname\u0026#34; \u0026gt; \u0026#34;/backup/$dbname-$(date -I)\u0026#34;.sql; done; find /backup -type f -mtime +1 -exec rm {} \\;\u0026#39; volumeMounts: - name: mariadb-backup-pv mountPath: /backup - name: secrets mountPath: /secrets readOnly: true restartPolicy: OnFailure volumes: - name: mariadb-backup-pv persistentVolumeClaim: claimName: mariadb-backup-pv-claim - name: secrets secret: secretName: kubesecrets Wordpress For my Wordpress sites, I need php extensions not provided by the official image. The Dockerfile and image I use are available in a public GitHub repository:\nhttps://github.com/jit06/docker-images/tree/master/wordpress https://github.com/jit06/docker-images/releases Below is an example of a Wordpress YAML definition I use:\nClick to expand code####################################### # Services ####################################### --- apiVersion: v1 kind: Service metadata: name: my_site namespace: prod spec: selector: app: my_site type: LoadBalancer ports: - protocol: TCP port: 8001 # exposed port for targetPort: 80 # targeting port on the container nodePort: 30200 # external IP port from 30000 till 32767 range ####################################### # volumes ####################################### --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my_site-data-pv-claim namespace: prod labels: app: my_site spec: accessModes: - ReadWriteOnce resources: requests: storage: 6500M storageClassName: local-storage volumeName: my_site-data-pv --- apiVersion: v1 kind: PersistentVolume metadata: name: my_site-data-pv namespace: prod spec: capacity: storage: 6500M volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage claimRef: name: my_site-data-pv-claim namespace: prod local: path: /media/pv_my_site nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - k3s-master-1 - k3s-master-2 ####################################### # containers ####################################### --- apiVersion: apps/v1 kind: Deployment metadata: name: my_site namespace: prod labels: app: my_site spec: replicas: 1 # how many replicas of pods we want to create selector: matchLabels: app: my_site template: metadata: namespace: prod labels: app: my_site # service will look for this label spec: containers: - name: my_site image: wordpress:v2024.11.17 ports: - containerPort: 80 env: - name: WORDPRESS_DB_HOST value: mariadb-internal-service - name: WORDPRESS_DB_NAME value: www.my_site.org - name: WORDPRESS_DB_PASS valueFrom: secretKeyRef: name: kubesecrets key: my_sitepwd - name: WORDPRESS_DB_USER value: my_site - name: NGINX_SERVER_NAME value: my_site - name: SITEMAP_DOMAIN value: www.my_site.org - name: WORDPRESS_DEBUG value: \u0026#39;false\u0026#39; - name: WORDPRESS_AUTH_KEY value: xxx - name: WORDPRESS_SECURE_AUTH_KEY value: xxx - name: WORDPRESS_LOGGED_IN_KEY value: xxx - name: WORDPRESS_NONCE_KEY value: xxx - name: WORDPRESS_AUTH_SALT value: xxx - name: WORDPRESS_SECURE_AUTH_SALT value: xxx - name: WORDPRESS_LOGGED_IN_SALT value: xxx - name: WORDPRESS_NONCE_SALT value: xxx volumeMounts: - name: my_site-data-pv mountPath: /srv/http/wordpress/wp-content volumes: - name: my_site-data-pv persistentVolumeClaim: claimName: my_site-data-pv-claim affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: pv operator: In values: - enabled requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: speed operator: In values: - fast Issues encountered so far As of now, after a few months of operation, I\u0026rsquo;ve encountered some issues, and all of them are hardware- or low-level related (e.g., driver issues). The table below lists these issues and the actions I took to resolve them:\nIssue Action USB nic (r8153) stopped working due to “Hardware Unit Hang” (reported by dmesg) Disabled TCP fragmentation offload. For now I added a line on rc.local file on all node (ethtool -K enx00e04c6800a0 sg off) I may set this on /etc/network/interfaces in the future\nOne USB NIC get reset by the kernel from time to time on node-1, which makes the vmbr1 to stop working even if it is up This triggers an alert from Zabbix. For now, I manually reset the interface and add it back into bond0: ifdown enx00e04c6800a0 \u0026amp;\u0026amp; ifup enx00e04c6800a0 ip link set enx00e04c680a0d master bond0 I may add some udev rules to handle this automatically, but since only one USB NIC has this problem, it might be best to “keep an eye” on the alerts. The NIC might be defective and could stop working soon.\nJMB582 controller (SATA) reported some failure (eg. ” failed command: READ FPDMA QUEUED”) This error is pretty common and usually caused by a bad SATA cable. I replaced it, and I haven’t seen the error occur again. ","permalink":"https://www.bluemind.org/cloud-home-pxe-proxmox-saltstack-k3s-minimum-toil/","summary":"\u003cp\u003eMy home lab was 7 years old and it was time to replace it. It was based on five Odroid HC1 nodes and 1 Odroid N1 (which never reached the mass production stage, Hardkernel sent it to me as a gift for a debug party). \u003ca href=\"/odroid-hc1-based-swarm-cluster-19-rack/\"\u003e4 HC1 nodes were used for a Docker Swarm cluster\u003c/a\u003e and 1 was dedicated to Nginx as a reverse proxy / WAF / SSL offloader. Regarding the Odroid N1, \u003ca href=\"/odroid-n1-enhanced-nas-19-rack/\"\u003eit was used as a NAS\u003c/a\u003e and also as a saltstack master.\u003c/p\u003e","title":"Cloud at home with minimum toil - PXE / Proxmox / Saltstack / k3s"},{"content":"I wanted to add some power consumption measures to my Openhab installation. But as for now, the majority of my installation relies on 433 Mhz devices and I did not found RFlink compatible ones that could send power consumption. By seing \u0026ldquo;Tuya Wifi\u0026rdquo; power plug on Aliexpress, I wanted to try one. Of course, these kind of device is \u0026ldquo;vendor locked\u0026rdquo;\u0026hellip; unless you open it and flash an alternative firmware.\nIn fact, there are a lot of similar models but they may house different microcontrollers : esp (32 / 8266), beken (bkxxx, blxxx), Realtec (rtlxxx), etc. The exact chip can usualy be identified with a quick search on the internet, and I discovered several opensource alternative firmwares. Each one support a limited set of microcontrollers (OpenBK7231T, Tasmota, libretiny, etc.).\nOpening the device Note that the model I ordered had no specific brand, and may not be available anymore. In general, they are pretty easy to open with a pair of pliers by just pulling a bit the plastic.\nThere is only one skrew that maintains the PCB (at the botton of the photo). I just removed it and the board was then easily removable.\nThe important mention was \u0026ldquo;T102_V1.1\u0026rdquo;. A google search brung me to the documentation of this microcontroller which is a Realtek RTL8710BN:\nhttps://fcc.report/FCC-ID/2AU7O-T102V11/4540736.pdf https://docs.libretiny.eu/boards/t102-v1.1/#pinout Preparing for ESPHome According to the documentation, here is the required pins :\nI soldered some dupond cables directly to the chip as it can be flashed in place:\nTo be able to flash the microcrontroller, it has to be put in \u0026ldquo;download mode\u0026rdquo;. This is triggered by putting TX2 to ground, powering on then releasing TX2 from ground.\nThis mode allows to get informations, dump the firmware or upload a firmware via the DEBUG UART port (TX2 / RX2).\nLike the documentation advices, I used a FT232RL usb to serial adapter because the other ones I have did not pass the initial handshake (timeout).\nBefore trying to flash ESPHome, which is the only opensource available alternative, I wanted to test the connection and backup the original firmware. To do so I installed libchiptool via python\u0026rsquo;s PIP tool.\npip install ltchiptool With this tool installed, I checked if the communication was working correctly:\npython3 -m ltchiptool flash info -d /dev/ttyUSB0 RTL8710B I: Connecting to \u0026#39;Realtek AmebaZ\u0026#39; on /dev/ttyUSB0 @ 1500000 I: Transmission successful (ACK received). I: Transmission successful (ACK received). I: |-- Success! Chip info: RTL8710BX I: Reading chip info... I: Chip: RTL8710BX I: Transmission successful (ACK received). I: Transmission successful (ACK received). I: +---------------------+--------------------------------+ I: | Name | Value | I: +---------------------+--------------------------------+ I: | Chip Type | RTL8710BX | I: | MAC Address | D8:D6:68:9A:E5:26 | I: | | | I: | Flash ID | 68 40 15 | I: | Flash Size (real) | 2 MiB | I: | | | I: | OTA2 Address | 0xFFFFFFFF | I: | RDP Address | 0xFFFFFFFF | I: | RDP Length | 0xFFFFFFFF | I: | Flash SPI Mode | QIO | I: | Flash SPI Speed | 100MHZ | I: | Flash ID (system) | FFFF | I: | Flash Size (system) | 2 MiB | I: | LOG UART Baudrate | 115200 | I: | | | I: | SYSCFG 0/1/2 | 40000200 / 02010301 / 00000001 | I: | ROM Version | V0.1 | I: | CUT Version | 0 | I: +---------------------+--------------------------------+ I: |-- Finished in 4.215 s Then I dumped the original firmware:\npython3 -m ltchiptool flash read -d /dev/ttyUSB0 RTL8710B smartplug.bin I: Connecting to \u0026#39;Realtek AmebaZ\u0026#39; on /dev/ttyUSB0 @ 1500000 I: Transmission successful (ACK received). I: Transmission successful (ACK received). I: |-- Success! Chip info: RTL8710BX I: Reading Flash (2 MiB) to \u0026#39;smartplug.bin\u0026#39; [########################################### 100% I: Transmission successful (ACK received). I: Transmission successful (ACK received). I: |-- Finished in 84.723 s Flashing ESPHome Despite ESPhome recommendation is to install from git, I prefered to use python\u0026rsquo;s PIP to do so.\npip install esphome Then I created a configuration file for the device and followed the instructions. Board type is rtl87xx and board id is t102-v1.1\npython3 -m esphome wizard smartplug.yml With the Yaml file, I compiled ESPhome firmware for the device :\npython3 -m esphome compile smartplug.yml And finaly uploaded this firmware to the device, using the same trick as for dumping (TX2 to ground, power on, then release)\npython3 -m esphome upload smartplug.yml Here is the output I got :\nConfiguring upload protocol... AVAILABLE: uart CURRENT: upload_protocol = uart Looking for upload port... Using manually specified: /dev/ttyUSB0 Uploading .pioenvs/powerplug1/firmware.uf2 |-- Detected file type: UF2 - esphome 2024.3.0 |-- Connecting to \u0026#39;Realtek AmebaZ\u0026#39; on /dev/ttyUSB0 @ 1500000 |-- Transmission successful (ACK received). |-- Transmission successful (ACK received). | |-- Success! Chip info: RTL8710BX |-- Writing \u0026#39;.pioenvs/powerplug1/firmware.uf2\u0026#39; | |-- esphome 2024.3.0 @ 2024-03-25 22:40:51 -\u0026gt; t102-v1.1 ###########################################|-- Transmission successful (ACK received). |-- Transmission successful (ACK received). |-- Transmission successful (ACK received). |-- Transmission successful (ACK received). | |-- Finished in 15.869 s =================== [SUCCESS] Took 16.86 seconds ================ INFO Successfully uploaded program. At this time a reset (unplug / replug) and voila ! The smartplug was running ESPHome. However, it just connected to the Wifi and was not reporting nor doing anything because the ESPHome Yaml file was just containing the bare minimum. EspHome need to know how to read sensors and how to drive the relay with GPIOs.\nChance is that someone actually documented a very similar device from another brand. In fact they are the same, so I used a similar YAML file to compile a new firmware and uploaded it.\nUnfortunately, the boot process stop complaining that \u0026ldquo;Change interrupts not supported\u0026rdquo;. This is a known issue that have been reported, but there is a workaround : https://github.com/libretiny-eu/libretiny/issues/155\nAfter the wiring_irq.c file modified as stated in the issue\u0026rsquo;s comments, the device booted correctly. Note that MQTT is not yet implemented for RTL87XX.\nIntegration with OpenHab This is the easiest part as an EspHome binding already exists. So In order to use this Esphome driven smartplug I just did the following in Openhab:\nInstalll the \u0026ldquo;ESPHome Binding for the native API\u0026rdquo; Add a thing and choose the ESPHome Binding Choose the \u0026ldquo;Add manually\u0026rdquo; option by selecting \u0026ldquo;ESPHome Device\u0026rdquo; Enter the hostname eg. \u0026ldquo;smartplug.local.lan\u0026rdquo; and the API password from the YAML file ","permalink":"https://www.bluemind.org/converting-a-tuya-power-plug-to-esphome-device/","summary":"\u003cp\u003eI wanted to add some power consumption measures to my \u003ca href=\"https://www.openhab.org/\"\u003eOpenhab\u003c/a\u003e installation. But as for now, the majority of my installation relies on 433 Mhz devices and I did not found RFlink compatible ones that could send power consumption. By seing \u0026ldquo;Tuya Wifi\u0026rdquo; power plug on Aliexpress, I wanted to try one. Of course, these kind of device is \u0026ldquo;vendor locked\u0026rdquo;\u0026hellip; unless you open it and flash an alternative firmware.\u003c/p\u003e\n\u003cp\u003eIn fact, there are a lot of similar models but they may house different microcontrollers : esp (32 / 8266), beken (bkxxx, blxxx), Realtec (rtlxxx), etc. The exact chip can usualy be identified with a quick search on the internet, and I discovered several opensource alternative firmwares. Each one support a limited set of microcontrollers (OpenBK7231T, Tasmota, libretiny, etc.).\u003c/p\u003e","title":"Converting a Tuya power plug to ESPHome device"},{"content":"A few years ago, I had the idea of building an Amiga like chassis with some arm based computer and a real mechanical keyboard. I even bought everything needed to build it ! The idea was to make an emulation machine similar to what exists for retrogaming consoles, but here I wanted something dedicated to old computers like Amiga, Atari, C64, CPC 6128. The Raspberry pi foundation brings the Pi 400, but I personally find that it lacks originality.\nThe time has come for this project to see the light : it\u0026rsquo;s now a reality and I love it :). As usual, I\u0026rsquo;m sharing all my work for anyone who would like to build one.\nOf course as you probably guessed, the name \u0026ldquo;Amstaga\u0026rdquo; is a mix of Amiga, Amstrad and Atari.\nOverview The build is about a fully usable ARM based computer with the following features.\nInside :\nExynos5422 ARM CPU with Mali-T628 GPU 2GB of Ram Emmc and MicroSD support Integrated 5V 4A power supply Integrated audio amp and stereo speakers On the rear :\nBoot selector switch to choose which media to boot from Reset button Gigagbit Ethernet 1x USB2 port HDMI output 3.5mm jack for stereo audio output Audio volume knob C8 connector for 110/220V AC plug Main power on/off switch On the sides :\n2.5 inches, half height SATA drive bay (SSD slot) PWM driven fan On the top :\n60% multicolor leds backlit mechanical keyboard 3 status leds (power, SATA, heartbeat) Custom logo area Requirements Hardware The hardware is pretty easy to find and not that expensive:\nAn Odroid XU4 : preferably the Q version as it has a bigger heat sink An Odroid Boom Bonnet kit A 60% usb keyboard (mine is a Magic-Refiner mk21, ref. 0769-82750507) A 20cm USB FPC cable, one end with USB-C and one end with type A (male - male) A 2.0MM Pitch, 30CM ribbon cable with 2x6 12P connector A 30x30x7mm DC 5V Brushless fan An IEC320 C8 AC power socket connector with integrated on/off switch A DC 5V 4A power supply 85x57x34 mm (mine has ref. PCB0025B) 3 x 3mm leds with corresponding resistors and sockets An USB3 SATA controller with cable (mine has ref. W2530P4) 15mm sliding on/off switch 12mm round momentary push button 4 Males and 2 females Molex 51021-0200 ended cables Optionnaly : 1 or more SATA SSD. I used some 120Gb Samsung EVO 850 Optionnaly : a micro SDCard Optionnaly : an Emmc Tools and materials The following things are needed to build the beast:\nA 3D printer or any way to print a surface of 30x16 cm 600 and 1200 sandpaper Primer / (white) paint / varnish (spray) 0.2 mm drill bit Some 2x5 mm wood skrews Hot glue Thin double faces adhesive Electrical tape Some thin wires Thermo-retractable sheath A good soldering kit : flux, solder, and thin iron tip A solder sucker / desoldering pump 3d printed model The model and sources can be found on Thingiverse : https://www.thingiverse.com/thing:6086511. I created it using FreeCad. I had to import some designs, like the XU4, to check how all parts could fit. I also created some others which I did not found on the internet.\nHere are two screenshots of the FreeCad model : on the left, the main part as it should be printed, on the right, a simulation with everything mounted on it, excepted the top cover:\nBelow are all the parts printed in PLA:\nOff course, to get something clean, I sanded all parts with 600 then 1200 grit to make them smooth. Then I applied some primer and white spray paint (3 layers). Finally, I drilled all the holes on skrew supports (by first placing each hardware pieces)\nAfter the paint job, the led socket can be mounted on the front cover:\nXU4 modding The XU4 board needs some modifications to be fully integrated.\nFirst, as the build includes an internal power supply, the barrel connector must be removed so the board can be aligned on the back of the shell. 2 wires are directly soldered on the motherboard to power it. The connector can be removed with a desoldering pump.\nThe two leds have to be removed because they will be exposed on the top of the case, in front of the keyboard. They can be removed by simply heating a bit both sides. Some wires are then passed thought the mounting holes of the power connector which have been previously removed.\nThe media boot selector switch must also be removed as it will be exposed on the back of the Amstaga. I personally bent it a bit and as the legs a pretty fragiles they break easily. In that case, the residus should probably be removed before soldering 3 wires (I my case, from an old IDE ribon cable)\nLast mod : the reset switch. It will also be exposed on the back of the Amstaga, but this time, it is not necessary to remove it. The legs are large enough to directly solder 2 wires on them.\nPower supply The power supply is composed of 2 parts : a standard C8 connector exposed on the back of the Amstaga, and 5V 4A AC/DC block which takes place inside the case.\nI had to bent 2 legs of the C8 connectors to make it fits correctly on the case.\nThe AC/DC block I used had a green led which I removed, but it is purely aesthetic (I just bent it until the legs broke)\nStatus leds To ease future maintenance and case opening, the status leds on the top of the case are not directly wired. The case has a dedicated space for a small connection modules that sits in between the boards\u0026rsquo; connectors and the leds. This allows to add the needed resistors and to make the leds easily removeable when the case have to be opened.\nThe connection module is based on a breadboard cut to 20x20 mm. It contains a 6 pins rail and 3 resistors.\nThe legs of the resistors can be used to bridge all the connections. The wires that will be linked the boards\u0026rsquo; connectors (from the XU4 and the SATA controller) will be soldered on the remaining holes.\nThe 3 status leds are inserted in the sockets and soldered to dupont cables, so they can be connected on the 6 pins headers before closing the case.\nChassis fan The Chassis fan is a small 30x30 cm I got on a raspberry pi cooling kit. It is wired to the CPU fan connector of the XU4, thus it needs the appropriate Molex connector (51021-0200).\nSATA Controler The SATA controler has a status led that should be removed for two reasons : the led is exposed on the top of the Amstaga and you probably don\u0026rsquo;t want to see the led blinking inside at the same time.\nThe led can be removed by heating both sides with a soldering iron.\nClock\u0026rsquo;s battery Like for the status leds, a dedicated space can hold a breadboard cut to 20x45mm for a CR2032 socket. There is some empty space because there was enough space on the design and I thought that I may add something else in the future\u0026hellip;\nThe same Molex connector as for the fan has must be used.\nSound Unlike the Pi 400 and the old Amiga and Atari like computers, the Amstaga has a built-in audio amp and speakers. It uses the Odroid Boom Bonnet kit : simple and pretty efficient for the size.\nI still did not undertood why, but I had to modify my boom bonnet. It was not working, or at least very badly. I found a way to make it works perfectly by removing the IDC connector and soldering the wires directly on the board. I also removed the wire on pin 9, which is I2S_0.CDCLK according to the schematic, as it was generating crackling sound (again, I did not found why).\nThe photos does not show the wire I removed because I just forgot to take a photo when I did find this trick.\nA kind of \u0026ldquo;hat\u0026rdquo; can be added to the volume knob to make it easy to manipulate from the back of the machine. This hat is part of the 3d files to print and is simply pushed over the existing one.\nIn order to wire the speakers, you have to make two male to female extension cables with molex 51021-0200.\nAssembly With all the elements ready, the assembly can be done in a few minutes. Note that on the first photos, you can see the 12p ribbon cable for the boom bonnet that goes under all boards, but that\u0026rsquo;s no more the case at the end. In fact, there were really bad sound when the cable was under the power supply. I guess that the AC-DC transformer generates some interferences to the I2S signal\u0026hellip;\nThe media boot selector and the XU4 with all soldered wires (power, leds and boot selection switch)\nThe power supply kit : the C8 connector is wired to the AC-DC bloc. The wires soldered on the XU4 intead of the barel connector are connected to the AC-DC converter output (which is tuned to output 5.25V).\nSome electrical tape should be added below the AC-DC block to better isolate it as it covers some wires.\nThe SATA Controller : it can be adjusted depending on how you want the SSD to overpass the case. When the controller is fixed, it\u0026rsquo;s easy to solder the wires for the SATA status led.\nLeds board, fan, reset button and clock\u0026rsquo;s battery board:\nGPIO ribbon cable and speakers that are glued with hot glue. You can also notice the 12p ribbon cable that connnect the boom bonnet to the XU4 that is no more passing under the AC-DC block.\nFinally, the two USB cables plugged in the USB3 ports : the FPC cable for the keyboard, and the standard USB3 to USB-C cable for the SATA controller.\nThe reason for an FPC cable for the keyboard is to be able to use a special 90° USB-C connector on the keyboard side to make it fits just under the cover, between the fan and the XU4 heat sink (more on that later)\nHere is the overview before closing. On the upper right corner, you can see the status led wires that are already connected to the small module board.\nClosing the case There are two things left to do before closing the case : putting the cover and the keyboard.\nThe cover is simply fixed with some thin double faces adhesive.\nThe keyboard is connected with the 90° USB-C connector which fits just fine between the fan and the XU4 heat sink. The keyboard is screwed to the case on the dedicated supports on each sides. Note that some keys have to be removed in order to screw the keyboard.\nThe last step : 4 plastic pads on the bottom, and the cover of the \u0026ldquo;extension\u0026rdquo; port (GPIO)\nThe beast and the final touch : a custom logo, like on Commodore Amiga :). The logo has been printed on photo paper. It is a simple Libreoffice file.\nSoftware part For now, I did very few things : running Ubuntu, adjust boot config not to mess with sd / emmc root and try Amiberry for amiga emulation.\nRoot partition handling My emmc has the official Hardkernel\u0026rsquo;s Ubuntu Mate desktop, and the micro SDCard has the Ubuntu minimal image.\nAs I mentioned earlier, the goal is to be able to switch boot and root devices with the emmc / microSD boot switch of the XU4. At the end, the result will be :\nemmc : boot and root are on the emmc with the official untouched Ubuntu Mate microSD : boot is on the microSD and root on the SATA drive with a customized emulator. With the default images, the boot is correctly selected by the switch, but the root partition can be mixed up depending on what is first found by the Kernel. So with both emmc and microSD inserted, you can boot from the emmc, but get the root partition mounted from the microSD.\nTo make sure that emmc boot get root on emmc:\nin boot.ini, set \u0026ldquo;root=/dev/mmcblk0p2\u0026rdquo; as kernel arguments in the \u0026ldquo;bootargs\u0026rdquo; env variable In /etc/fstab, set \u0026ldquo;/\u0026rdquo; to mount \u0026ldquo;/dev/mmcblk0p2\u0026rdquo; and \u0026ldquo;/media/boot\u0026rdquo; to mount \u0026ldquo;/dev/mmcblk0p1\u0026rdquo; Now, here is how to make root partition to be on the SATA drive when booting from the microSD.\nBoot from emmc, make at least one partition on /dev/sda with either fdisk or cfdisk and format it (e.g: mkfs.ext4 /dev/sda1).\nOn the microSD, prepare for root partition on /dev/sda1 (or any partition you previously made):\nin boot.ini, set \u0026ldquo;root=/dev/sda1\u0026rdquo; as kernel arguments in the \u0026ldquo;bootargs\u0026rdquo; env variable In /etc/fstab, set \u0026ldquo;/\u0026rdquo; to mount \u0026ldquo;/dev/sda1\u0026rdquo; and \u0026ldquo;/media/boot\u0026rdquo; to mount \u0026ldquo;/dev/mmcblk1p1\u0026rdquo; Then, copy all data from /dev/mmcblk1p2 to /dev/sda1 as root :\nmkdir /mnt/dst mkdir /mnt/src mount /dev/mmcblk1p2 /mnt/src mount /dev/sda1 /mnt/dst rsync -avxHAX --progress /mnt/src /mnt/dst/ umount /mnt/dst /mnt/src The second partition on the microSD will no more be used. Booting from the the microSD will use /dev/sda1 as root. You can now duplicate the SATA SSD and start customizing each one in order to get different experiences by changing the SDD before booting.\nI personnaly use the second partition of the microSD to exchange data between all OSes I boot on the machine.\nInstalling Amiberry I\u0026rsquo;m just sharing here my first experience using Amiberry to test Amiga emulation on my Amstaga. Let\u0026rsquo;s say that it is a kind of proof of concept to run an emulator on it. I did nothing to make it start automatically, I will cover this in future blog posts.\nFirst things first : doing a full upgrade is a good idea. As mentioned before, the SATA drive I\u0026rsquo;m using contains an Ubuntu minimal from hardkernel, so the upgrade takes a few minutes:\napt-get update \u0026amp;\u0026amp; apt-get upgrade reboot Amiberry is not on the repository and must be built from sources. I bascilaly followed the official guide on https://github.com/BlitterStudio/amiberry.\nInstalling dependencies :\napt-get install build-essential git libsdl2-2.0-0 libsdl2-ttf-2.0-0 libsdl2-image-2.0-0 flac mpg123 libmpeg2-4 libserialport0 libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev libflac-dev libmpg123-dev libpng-dev libmpeg2-4-dev libserialport-dev libpam-systemd xserver-xorg-core xinit libdrm-exynos1 xserver-xorg-video-armsoc-exynos xserver-xorg-video-armsoc xserver-xorg-input-all x11-xserver-utils xfonts-100dpi mali-x11 evilwm libgles2-mesa-dev libgles1 libgles2 Note that I installed \u0026ldquo;evilwm\u0026rdquo; as a lightweight windows manager because SDL2, which Amiberry uses, needs a windows manager to handle keyboard events (e.g: F12 to get into Amiberry GUI).\nBuilding from github sources (don\u0026rsquo;t try -j8 to build it faster, the XU4 does not have enough RAM):\ncd /root git clone --depth 1 https://github.com/BlitterStudio/ amiberry cd amiberry make -j4 PLATFORM=xu4 Launching Amiberry should works now, but you will notice that it is very slow in rendering graphics. This is because it chooses the first available SDL2 rendering engine. And as the default Ubuntu SDL2 provides several engines in addition to GLES, this makes Amiberry starting with OpenGL (software rendering) and not GLES (hardware rendering on XU4).\nFor now, the easiest solution I found is to compile the SDL2 library without OpenGL and Vulkan support. Thus, the library is built with only GLES support and Amiberry becomes faster.\nHere is how to compile SDL2 from sources on the XU4:\ncd /root git clone --depth 1 https://github.com/libsdl-org/SDL.git -b SDL2 cd SDL ./configure --disable-video-opengl --disable-video-vulkan make -j4 make install The default install path is /usr/local/lib, whereas the original Ubuntu library is in /usr/lib/arm-linux-gnueabihf. A simple a (very) dirty solution is to copy libSDL2.so from /usr/local/lib to /usr/lib/arm-linux-gnueabihf. The best way should be to modify /etc/ld.so.conf, but as I say, this is a kind of PoC, so I went quick and dirty :).\nTo start Amiberry directly, you must launch Xorg server and evilwm. I made a simple script for that in /root/amiberry, named \u0026ldquo;start_amiberry.sh\u0026rdquo;:\n#!/bin/bash xsetroot -cursor_name X_cursor # force a default cursor /usr/bin/evilwm \u0026amp; # launch the windows manager exec taskset -c 4-7 /opt/amiberry/amiberry # launch Amiberry on BIG cores only To launch Amiberry as root from the console :\nxinit /root/amiberry/start_amiberry.sh $* -- :0 vt$XDG_VTNR Other considerations Why an XU4 ? When I first had this idea, it was the most powerfull SBC available. Moreover the way it is organized is perfect for such a build :\n2x USB that are on the opposite side of all other connectors that need to be exposed. This allows to plug the keyboard and the SATA drive internally. It has a boot media selector to switch between Emmc and MicroSD. Thus it is easy to keep a supported OS on the Emmc and use the SDCard to boot with the inserted SATA drive as root It has a PWM FAN connector which makes it easy to drive the chassis fan. Why a 60% keyboard instead of a full width one ? It is more an aesthetic choice. I like the Amiga 600, and I wanted something like it. Moreover, a full width keyboard would have required a larger case, beyond 30cm. This could have been more difficult to print.\nWhy a half height 2.5 inches bay instead of full height ? Again it\u0026rsquo;s mainly aesthetic : with a bigger drive bay, the whole case should have been taller and I less \u0026ldquo;cute\u0026rdquo;. Also, nearly all SSD are half height nowdays, and regarding their prices, I saw no interest in using HDD.\nFinal result Running Ubuntu on the Emmc and AmigaOS 3.2 on SD/SATA Drive (on a good old 4:3 display, in 1280x1024) !\nFirst videos Here are 3 videos : one showing the sound output on the official Ubuntu from Hardkernel, one showing a run of Amiberry with Amiga OS 3.2 and thge last running an Amiga Demo (Arte Sanity) without any tuning (I just activated the floppy disk sound for fun)\n","permalink":"https://www.bluemind.org/amstaga-odroid-xu4-powered-neo-retro-computer-amiga-style/","summary":"\u003cp\u003eA few years ago, I had the idea of building an Amiga like chassis with some arm based computer and a real mechanical keyboard. I even bought everything needed to build it ! The idea was to make an emulation machine similar to what exists for retrogaming consoles, but here I wanted something dedicated to old computers like Amiga, Atari, C64, CPC 6128. The Raspberry pi foundation brings the Pi 400, but I personally find that it lacks originality.\u003c/p\u003e","title":"Amstaga : Odroid XU4 powered neo-retro computer - Amiga style !"},{"content":"A few years ago, I bought among other things a 8\u0026quot; 4:3 LCD display for a new retro portable console project. However I abandoned this project as more and more good commercial solutions where released, including the Steam Deck. Recently, while I was browsing Thingiverse, the magic of opensource philosophy appeared once again : someone did a 3D printable model of an mini bartop that requires the exact same LCD panel I had purchased !\nSo I could not resist to build one\u0026hellip;\nUsed materials The 3D model I used can be found on thingiverse, thanks to the author who shared this excellent design : https://www.thingiverse.com/thing:4295854\nHardware parts A 8\u0026quot; 4:3 LCD pannel with a 1024x768 resolution. It may still be found on Aliexpress 8 x 24mm arcade push buttons A standard arcade micro-switch based joystick A cheap Arcade to USB HID controller A 3W numerical audio amp with embeded volume knob (PAM8403) 2 x 36mm 3W speakers A Raspberry pi 4 (any model should be fine) A fast microSD. I used a 64Gb, which is enough to store hundreds of arcade games Some wires and dupont cables 3.5mm stereo jack Some wood skrews Build parts A 3d printer that can print 20x20x18 cm Paint primer : I used Vallejo 018010 (white) Spray paint : I used Edding 5200, good quality and good price Hot glue Inkjet printer and a sheet of transparent adhesive paper Double sided adhesive 3D printed parts All the parts have been printed in PLA :\nPaint job Before painting, I sanded all the parts and applied the paint primer :\nWith 3 layers, all the parts were correctly printed. The bottom still had visible layer lines, but it\u0026rsquo;s the bottom :)\nBottom In order to get a good grip on surface where the bartop will be used, I added 4 rubber pads on the bottom, next to each screw hole. Then I screwed the Pi directly using the dedicated holes.\nBack panel The back of the cabinet has 2 holes for 34mm speakers. Unfortunately, I didn\u0026rsquo;t find any good speakers of this size. I used 36mm speakers : the speaker edges just fit the flange so the moving part stay free. I glued them with hot glue.\nAssembly The sides are simply skrewed with the back panel. Each side has a groove in which the bezel of the LCD display fits perfectly.\nI used some thick double sided adhesive to fix the display controller and the HDMI interface boards.\nControl panel The control panel is large enough to fit 6 arcade buttons (24mm), but to fit the stick, the metallic plate must be removed. The stick is then skrewed directly to the panel.\nThe buttons are just clipped on the holes, and the USB HID controller board is also fixed with double sided adhesive.\nSound amplifier A small 3.5mm jack is used to get the audio from the standard Raspberry Pi analog output\nThe amp need 5V to operate : I used two dupont cables and plugged them on the GPIO pins 4 (+5v) and 6 (ground).\nThe volume knob of the amplifier comes with a bolt to mount it easily. Unfortunately, it does not provide a good grip, because of a bad quality skrew thread. So in addition, I used some hot glue to firmly fix it.\nWiring overview At this point, the last thing to wire was all the buttons as well as the stick: here is an overview of all hardware parts mounted and wired :\nThe USB-C power supply cable is directly plugged in to the Pi. There is a small hole in the back panel to pass it. Note that a small loop on the bottom panel is used to fix the power cable with a small rilsan necklace\nJust before closing the cabinet with the bottom panel : all fit nicely, but there is no space left !\nDecorations The idea was to build a Ninja Turle themed cabinet, hence the purple cabinet and green buttons.\nI used some images from the internet and printed them with my inkjet printer on a transparent adhesive paper.\nSee it in action ","permalink":"https://www.bluemind.org/mini-3d-printed-arcade-bartop/","summary":"\u003cp\u003eA few years ago, I bought among other things a 8\u0026quot; 4:3 LCD display for a new retro portable console project. However I abandoned this project as more and more good commercial solutions where released, including the Steam Deck. Recently, while I was browsing \u003ca href=\"https://www.thingiverse.com/\"\u003eThingiverse\u003c/a\u003e, the magic of opensource philosophy appeared once again : someone did a 3D printable model of an mini bartop that requires the exact same LCD panel I had purchased !\u003c/p\u003e","title":"Mini 3D printed arcade bartop"},{"content":"As a fan of retrogaming, I\u0026rsquo;m using an old CRT TV to play with my retro consoles (Famicom, Megadrive, etc.). My TV is a not so old Grundig ST 72-864 (board reference : CUC2033). It is a big 70cm display with exellent stereo sound and a nice picture rendering. It even provides a way to easily access geometry settings with the remote control.\nUnfortunately, the TV started to behave strangely and was switching to standby mode after a few minutes. I managed to repair it and I thought that this was worth sharing this story\u0026hellip;\nSymptoms / problem All was working wonderfully when the TV started to \u0026ldquo;hiccup\u0026rdquo;, like a mini degauss after about 20 minutes of use. These \u0026ldquo;hiccups\u0026rdquo;, were more and more frequent until the TV started to swich to standby mode. Also, there was a \u0026ldquo;tic\u0026rdquo; sound at each \u0026ldquo;hiccup\u0026rdquo;.\nI was able to switch it back on, but after a few seconds, same symptoms, same standby mode.\nI opened the TV, and searched for any visual sign\u0026hellip; And I found that the responsible for the \u0026ldquo;tic\u0026rdquo; sound was a spark on the high voltage transformer (more info on wikipedia).\nBelow is the TV\u0026rsquo;s motherboard, the spark was visible in the area surrounded in red :\nParts replacement Board clean-up First things first, I started to cleanup the board with a \u0026ldquo;contact spray\u0026rdquo; from Facom and a tooth brush, which gave a pretty good result :\nHigh voltage transformer replacement Searching the web for the reference of the TV (ST72-864) and the board reference (CUC2033), I managed to find the service manual with all parts references. And guess what, the same brand new high voltage transformer could still be bought in 2022 ! I found it on http://www.donberg.ie (ref HR6517).\nI desoldered the old one with a solder sucker, then soldered the new one :\nCapacitors As we all know, capacitors do not have an infinite life. So, while the main board was disconnected, I also deciced to replace all capacitors (about 50). I know that \u0026ldquo;if it\u0026rsquo;s not broken, don\u0026rsquo;t fix it\u0026rdquo;, but I did not want to risk any other failure or damage because of a leaking capacitor. CRT TV are becoming rare and expensive theses days !..\nOn / off switch Ok that\u0026rsquo;s another \u0026ldquo;fix whereas it\u0026rsquo;s not broken\u0026rdquo;, but I was advised by a former CRT repairer that this very model had a known weakness with its power switch (if you read french, here are his notes).\nThis TV does not use a simple ON / OFF switch, but a fleeting action switch. The known issue with this switch is that it tends to have a small current leak after several years. This current leak makes the processor crashes and randomly put the TV in standby mode (yet another cause for this symptom).\nSo again, while the TV was opened, I deciced to change this power switch. I found a brand new one with its reference (5000636) on a french web site : https://www.indipc.fr\nBelow is the board with the original switch :\nHere, the original switch (the black one) has been desoldered. The new one (white) is identical, except that it has a kind of metallic collar that must be removed :\nThe new switch in place : it looks like the old one\nCalibration With a new power supply for the CRT and all brand new capacitors, the old geometry settings was no more suitable. But I was not able to correct them via the service menu : the picture was like \u0026ldquo;compressed\u0026rdquo; in the middle :\nTo correct the picture, I had to \u0026ldquo;play\u0026rdquo; with the two adjustable \u0026ldquo;things\u0026rdquo; (surrounded in red) bellow.\nThe one at the bottom right is the \u0026ldquo;+A\u0026rdquo; value. The technical manual says that this should be adjusted so the voltage on R60037 or R61313 reads 142V. In my case, I had to set it so I read 139V.\nThe one on the top left is the tuning coil. The manual says that this should not be tuned because it is supposed to have a good factory value. This coil allows to adjust the width of the picture. On my TV, tuning it adjusted the width of the picture but not (or very little) on the corners.\nTogether with \u0026ldquo;software\u0026rdquo; geometry adjustements throught the service menu (the code is 8500), I managed to get a pretty decent result :\nBelow are the geometry values with a standard 240P picture from my Playstation 1 in background (running Ridge Racer) :\nThat\u0026rsquo;s it : I hope this CRT will now last another 20 years ;)\n","permalink":"https://www.bluemind.org/repairing-crt-display/","summary":"\u003cp\u003eAs a fan of retrogaming, I\u0026rsquo;m using an old CRT TV to play with my retro consoles (Famicom, Megadrive, etc.). My TV is a not so old Grundig ST 72-864 (board reference : CUC2033). It is a big 70cm display with exellent stereo sound and a nice picture rendering. It even provides a way to easily access geometry settings with the remote control.\u003c/p\u003e\n\u003cp\u003eUnfortunately, the TV started to behave strangely and was switching to standby mode after a few minutes. I managed to repair it and I thought that this was worth sharing this story\u0026hellip;\u003c/p\u003e","title":"Repairing an old CRT display"},{"content":"It has been a long time since I wanted to build my own Visual Pinball cabinet. I finally took the time, but as I don\u0026rsquo;t have enough space for a \u0026ldquo;standard\u0026rdquo; sized pinball, I made a miniature one, a kind of bartop pinball.\nThis is the story of my build, not exactly a recipe, but it may inspire anyone who is thinking to build one\u0026hellip;\nHardware requirements Tools \u0026amp; Materials So far, here is the main things I used for this build :\nChipboard wood, 10mm. I used one 80x160cm board. Any other type of wood could have been used, but I found this kind of wood feels more authentic when painted. Plastic brackets Pine wood cleats Various wood skrews Circular saw Jigsaw 3 x 400ml white spray paint (satin finish) A bit ofbBlack spray paint PC Hardware The hardware I used is by far not the most powerfull, but it\u0026rsquo;s enough to run all the pinball tables I tested at 60 fps in (near) Full-HD resolution, while staying cool and quiet.\nPlayfield display : CHIMEI CMV 222H, 22\u0026quot;, 1680x1050 Backglass display : Dell 1708FP, 17\u0026quot;, 1280x1024 Motherboard : ASRock B560M-ITX/ac 2 x 4Gb DDR4 3200 ram modules (Kingston hyperX fury) Kingston A2000 NVMe SSD M.2, 500GB (a 250 would have been enough) Pentium G6400 Zotac GeForce GTX 1050 TI Mini (4 Gb) be quiet! Pure Power 11 (400 W) Other hardware parts 7 backlit Sanwa arcade buttons, 4 blues and 3 whites (33mm) 1 big blue backlit button (45mm) 1 small push button for ATX power-on (10mm) Usb ports for dashboard panel A true pinball plunger A 0.6X4X300mm spring An ADXL335 GY-61 accelerometer A Teensy LC A pair of 10 cm Pioneer car speakers (TS-G1020F) A 75mm sliding potentiomer (Fader) SC6080GH An 12V numerical audio amp (TDA7297), 2x15W 4 plastic washers to put the motherboard on (about 5mm high and with a diameter of 1.2cm) Initial plans Plan and dimensions The initial plans I made are available for download here. This is a simple Libreoffice document.\nWood cut plan As I used a single board of 160x80cm, I planned the cut as following. The goal was to get clean border as much as possible for visible parts. Again, the file can be downloaded here. Plexiglass cut plan I used some plexiglass to protect display pannels. I used a 100x50cm board, so below is how I cut needed pieces :\nThe Build Wood cut To cut all parts, I used both a circular saw and a jig saw. As you can see on the first photo, for the parts that had to be identical, I filed them together. On the right, all pieces of wood, ready to be assembled :)\nBackglass structure All the parts are assembled using plastic brackets. The wood is far too thin to skew on the side. Here is the assembly of the bacckglass structure.\nIn order to fix the display inside the backglass, I used two pine wood cleats and screwed them through the VESA holes. Due to an error, the wood cleats are not perfectly straight, but when all is fixed, the screen is well aligned, which is what is the most important at the end.\nFinaly, I did one hole on the bottom part of the case to pass power and DVI cables, and several small holes on the back to get some air flow to keep the display cool.\nPlayfield structure The play field structure is assembled the same way as the backglass. However, here the wood cleats used on both side serve as a bed for the screen. The width of the cabinet is tight enough to let the screen fit with a little bit of force to be inserted. This ensure that the screen won\u0026rsquo;t move.\nI also used a wood cleat on the back of the cabinet. Its purpose is to rigidify it, but also to screw the backglass structure on it later.\nPlayfield holes The playfield strucure being ready, the next step was to drill all the required holes before painting. I found it easier to make these holes on the mounted structure to better juge and appreciate their placement.\nI noted all the dimensions I used directly on the wood. They should be easily viewable by clicking on any photo. Excepted for the speakers, I made the holes with a driller.\nOn each side : left / right flipper and left / right magna save buttons\nOn the front : exit, coin, pause and start buttons as well as the plunger :\nBack on the sides : holes for the speakers (I used a jig saw this time) :\nAnd last but not least : one hole on the right side for two USB ports\u0026hellip;\n\u0026hellip;and another one on the left side for the ATX power-on button (10mm momentary push button) :\nPlunger system Some \u0026ldquo;analog plunger\u0026rdquo; can be bought on Visual Pinball specialist websites, but the one I made is cheaper (less than 17 euros) and more customisable :).\nBasicaly, the plunger system consists in a slider potentiometer (fader) moved by a real plunger. However, the potentiometer has to be at the good height and body part must not move while actioning the plunger.\nThe fixation system is composed of a piece of 10mm chipboard wood sandwitched between two pieces of pine wood. The potentioneter is simply laid on the top and has an overflow of about 1mm regarding the pieces of pine wood. Then two metal bars are screwed over on each side, so the potentiometer body can\u0026rsquo;t move and the slider is totaly free. Also, the legs are still accessible to solder some wires.\nThen, I used two metal squares to fixe the structure inside the cabiner.\nThe whole thing mounted inside the cabinet : the slider is swandwitched between the metal ring and the plastic sleeve :\nAs I used a plunger made for real pinball, the spring was very hard. In fact, when I tried to pull it, the whole cabinet was coming ! So I replaced the original spring by a softer one. I used a 0.8x12x305mm spring that I cut a bit shorter than the original. The result is a nice plunger that can be pulled quite easily, but still react fast enough to simulate a real plunger with the potentiometer.\nPower supply supports \u0026amp; mod For a matter of space, the power supply must be mounted vertically. I could not fix in on the back pannel because I wanted to make it easy to open. So I used some kind of metal square that are normally used in carpentry. I did a hole in each to be able to screw the power supply on them.\nThe power supply provides current for the motherboard, but the two LCD pannels also needed some juice. In order to get only one plug on the back of the canbinet for all, I modified the power supply by adding two additional c13 cables directly at the source. Two holes are drilled in the metallic shield to pass the wires\nPlayfield backdoor As a lot of things generate heat in this small cabinet (2 screens, cpu, graphic card, audio amp\u0026hellip;), I added a standard 100mm fan to the playfield backdoor.\nAs I said earlier, I wanted the backdoor to be removable, so I could access to the inside even with everything in place. The simplest way I found was to use two closet door magnets on two corners. The backdoor is then simply held by the magnets.\nThe hole for the power supply is not the prettiest part, but it\u0026rsquo;s in the back, and is rarely seen.\nThe backdoor being removable, I had to make the fan\u0026rsquo;s wire longer to reach the motherboard connector.\nMotherboard and Graphic cards The motherboard is fixed using screws from the bottom of the cabinet. The screws are fixed with bolts on the cabinet. The motherboard is laid on plastic washers, and other bolts maintain it. The plastic washers prevents the motherboard from touching the woods (air flow).\nThe graphic card take its support from a metal square screwed in the wood and is placed far enough from the side to let the air flow for heat dissipation.\nHere is an overall view of the power supply, motherboard and graphic card mounted into the cabinet :\nAudio Amp A pinball game is nothing without a good sound system. The numerical amplifier has been screwed on the back of the cabinet (to the left of the power supply and below the fan), so the volume knob could be accessible. I was surprised how well the TDA7297 drives the pair of Pioneer car speakers and the cabinet serves as a nice subwoofer when all is closed.\nPaint / buttons / speakers Before painting, I put some protection adhesive over all holes in order not to paint all the inside (skrews, etc.)\nI had to use a bit less than 3 x 400ml spray paint for the whole cabinet. Maybe standard paint with a good brush could have been cheaper. However, spray paint has a very good finish\u0026hellip;\nThe bottom of the cabinet has only had one layer of paint because I did not see any interest in making it \u0026ldquo;perfect\u0026rdquo; has it\u0026rsquo;s the bottom which is never seen. Note that I added 4 small plastic legs with ajustable height. This makes the pinball more stable whatever the surface it is on.\nAt this point, I mounted all the controls, the speakers, the usb port (on the left) and the power button (on the right). The small hole bellow the big blue button host a 3mm blue led inside its metal bed (directly inserted in the wood)\nControls brain The \u0026ldquo;brain\u0026rdquo; that drives all the buttons is based on a Teensy LC to expose a standard HID joypad input to Windows (more on that in the \u0026ldquo;Software\u0026rdquo; chapter bellow). An accelerometer (ADXL335 GY-61) is wired to it via 2 analog inputs to handle tilt.\nI also made a standarc USB A port mini board to wire it directly on a free USB extention on the motherboard, as there were not enough space on the back of the motherboard to use one of its USB connectors.\nThe GPIO ports of the the Teensy have been exposed on the breadboard to solder all the buttons\u0026rsquo; wires. Each wire has a red electric pod on its end, which is directly pluggable onto any button. The ground is solderered to the Teensy and basicaly consists in a chain of blue pods\nEvery button is backlit with a led, which means additional wires from a 5V source. The ATX power supply provides the required current, so I used one of the power supply plug to build a chain of pods for +5V and ground.\nFinally, here is the \u0026ldquo;brain\u0026rdquo; fixed on the bottom of the cabinet, with the plunger\u0026rsquo;s potentiometer also soldered to one analog input of the Teensy\nBelow is an overview of all the controls wired\u0026hellip;\n\u0026hellip;And here an overview of the cabinet before putting the playfield screen :\nPlayfield display The playfield screen is inserted with just enough force to be held in place. The photo on the right show the inside of the cabined from the back, with the screen in place.\nThe screen is protected with a 2mm piece of plexiglass. I cut it with a cutter using a rule. After 2 or 3 passes, the plexiglass can be broken straight. The plexiglass is placed over the screen and cover all the cabinet width. It is fixed only with the edge metal bars (see below).\nBackglass mounting The backglass screen is also protected by a piece of plexiglass, but this time it had to be maintened vertically. I used some black plastic edges on the inside so the plexiglass can be fixed with double sided adhesive tape.\nAs the plexiglass is taller and wider than the effective display surface of the screen, I painted the \u0026ldquo;margins\u0026rdquo; with black spray paint. The plexiglass being protected by a thin plastic film, I removed it only on the parts to be painted. I added some adhesive to ensure a good protection before painting.\nThe backglass part of the cabinet is screwed from the bottom with screws and bolts. Below are the holes I made for this purpose\nAnd here is the backglass part fixed to the cabinet\nEdge and corner finish The original edges and corners of chipboard wood are not really nice. In order to get a more polished / industrial finish, I glued some piece of brushed aluminium on all egdes. For the playfield, I let just enought space to be able to slide the plexiglass.\nFor the corners, I used some metal \u0026ldquo;protections\u0026rdquo;, I guess they are normally used to protect and / or assemble furtinure corners.\nDecoration Stickers I know that \u0026ldquo;simple is beautiful\u0026rdquo;, but it\u0026rsquo;s not enought \u0026ldquo;arcade\u0026rdquo; :). I wanted to put a small touch of color on the cabinet. Basicaly I took some picture from the internet, and printed them on a transparents adhesive paper with an inkjet printer.\nI put the décoration on the top of the backglass and on each side\nSoftware part Preparing Windows 10 (pro) By default Windows 10 comes with a lots of features that could be either disturbing or cause slower booting for a dedicated gaming machine like a visual pinball cab.\nHere is what I did on mine :\nDisabled \u0026ldquo;news and interest\u0026rdquo; Uninstalled all possible applications from stock install Lock screen settings : Turned off lock screen background picture and \u0026ldquo;fun facts, tips and more\u0026rdquo; In the task bar settings : set to automatically hide, and use \u0026ldquo;small taskbar\u0026rdquo; In the theme settings : I disabled all standard desktop icons from \u0026ldquo;Desktop icon settings\u0026rdquo; Some services can also be stopped (command line):\nsc config \u0026#34;stisvc\u0026#34; start=disabled sc config \u0026#34;iphlpsvc\u0026#34; start=disabled sc config \u0026#34;lmhosts\u0026#34; start=disabled sc config \u0026#34;SCardSvr\u0026#34; start=disabled sc config \u0026#34;DiagTrack\u0026#34; start=disabled sc config \u0026#34;RasMan\u0026#34; startdisabled sc config \u0026#34;BthAvctpSvc\u0026#34; start=disabled sc config \u0026#34;diagnosticshub.standardcollector.service\u0026#34; start=disabled sc config \u0026#34;WbioSrvc\u0026#34; start=disabled sc config \u0026#34;PcaSvc\u0026#34; start=disabled sc config \u0026#34;NetTcpPortSharing\u0026#34; start=disabled sc config \u0026#34;bthserv\u0026#34; start=disabled sc config \u0026#34;DPS\u0026#34; start=disabled sc config \u0026#34;TabletInputService\u0026#34; start=disabled sc config \u0026#34;Spooler\u0026#34; start=disabled sc config \u0026#34;WSearch\u0026#34; start=disabled Finaly I followed the official documentation for autologon : https://docs.microsoft.com/en-us/sysinternals/downloads/autologon\nTeensy LC HID sketch Using the Teensy as a standard USB joypad is pretty easy with the included \u0026ldquo;joystick\u0026rdquo; library that comes with the IDE. They are plenty of exemples on the internet, below is my code :\nconst int numButtons = 8; // left pin x2, right pin x2, start, coin, pause, exit byte allButtons[numButtons]; void setup() { Serial.begin(9600); Serial.println(\u0026#34;init controler...\u0026#34;); Joystick.useManualSend(true); for (int i=0; i\u0026lt;numButtons; i++) { pinMode(i, INPUT_PULLUP); } Joystick.X(512); // init nudge X (tilt) Joystick.Y(512); // init nudge Y (tilt) Joystick.Z(512); // init plunger // neutral dpad as is not used and should not disturb key dectection during settings Joystick.hat(-1); } void loop() { // nudge Joystick.X(analogRead(2)); Joystick.Y(analogRead(3)); // plunger Joystick.Z(analogRead(4)); // read digital pins and use them for the buttons : 1 = not pressed, 0 = pressed for (int i=0; i\u0026lt;numButtons; i++) { if (digitalRead(i)) { allButtons[i] = 0; } else { allButtons[i] = 1; } Joystick.button(i + 1, allButtons[i]); } Joystick.send_now(); // runs 200 times per second delay(5); } Visual Pinball softwares There are tons of documentations on the internet, so I just give here some things specific to my build.\nMain engine and Frontend To make it simple, I chose to only use one engine : Visual Pinball X (VPX). It has a lot of tables, at least enough for me.\nFor the Frontend, I used pinballX which I found simple to use and to configure. It even allows to generate video previews of the tables you installed !\nHere is the guide I followed : https://www.vpforums.org/index.php?app=tutorials\u0026amp;article=160\nIf you are discovering the world of visual pinball, a full table consists of :\nA \u0026ldquo;.vpx\u0026rdquo; file which defines the table that Visual Pinball X runs A \u0026ldquo;.direct2bs\u0026rdquo; file which provides the backglass display material Eventualy, a PinMame rom to drive the DMG display and the game\u0026rsquo;s logic Graphics settings Regarding the hardware described in the begining of this post, here are the main settings I used:\nIn vpx : set to \u0026ldquo;high end PC\u0026rdquo; In the nvidia control panel : power management mode is set at \u0026ldquo;prefer maximum performance\u0026rdquo; and \u0026ldquo;Low latency mode\u0026rdquo; is set to on With the \u0026ldquo;Flingston\u0026rdquo; table, I get between 110 and 180 fps without V-Sync, so a constant 60fps with V-Sync and a cool gfx card.\nAlso, my playfield screen being \u0026ldquo;small\u0026rdquo; and with a 16:10 ratio instead of 16:9, I zoomed all the tables to maximize display size without compromising the gameplay.\nSome TIPS As I found some issues during my build, I decided to share them together with the solutions :\nif the directB2S backglass does not appear: make sure you selected the right screen in B2S_Setup, and check that your \u0026ldquo;.directb2s\u0026rdquo; files have the same name as the table file if the DMD display does not appear for pinball with no pinmame rom : check that \u0026ldquo;enabled=true\u0026rdquo; is present in VPinMAME\\DmdDevice.ini If you have lot of audio noise with the analog stereo out : set audio to AC97 instead of HD Audio in the BIOS \u0026ldquo;Pause\u0026rdquo; is not really possible with VPX. You can map exit button that actually pauses the game, but if you want direct exit, it will quit directly You can add \u0026ldquo;-exit\u0026rdquo; in as argument for VPX start command, so you can exit directly by pressing the exit button (no menu) You may want a \u0026ldquo;open door\u0026rdquo; button to access some pinball settings. I mapped it to one button on the front. I used others existing buttons to map up, cancel and enter (do not map them to \u0026ldquo;exit\u0026rdquo; or \u0026ldquo;left\u0026rdquo;/\u0026ldquo;right\u0026rdquo; pinball) You can use \u0026ldquo;-extminimized\u0026rdquo; argument for VPX command to start it minimized from pinball X Results ","permalink":"https://www.bluemind.org/homemade-mini-visual-pinball-system/","summary":"\u003cp\u003eIt has been a long time since I wanted to build my own Visual Pinball cabinet. I finally took the time, but as I don\u0026rsquo;t have enough space for a \u0026ldquo;standard\u0026rdquo; sized pinball, I made a miniature one, a kind of bartop pinball.\u003c/p\u003e\n\u003cp\u003eThis is the story of my build, not exactly a recipe, but it may inspire anyone who is thinking to build one\u0026hellip;\u003c/p\u003e\n\u003ch2 id=\"hardware-requirements\"\u003eHardware requirements\u003c/h2\u003e\n\u003ch3 id=\"tools--materials\"\u003eTools \u0026amp; Materials\u003c/h3\u003e\n\u003cp\u003eSo far, here is the main things I used for this build :\u003c/p\u003e","title":"Homemade mini Visual Pinball system"},{"content":"Some years ago, I built a custom shield for an arduino mega running Rflink. The purpose was to provide Rflink MQTT messages over ethernet. I recently built a new version which is more compact and with additional features.\nI replaced the arduino nano by a Wemos D1 mini (esp8266). I also used an arduino mega pro, which is smaller than the orignal one. The 433Mhz hardware has been changed to by SRX 882 and STX882 as they provide a better range than RXB6 and XD-FST FS1000A.\nMain features are : Provides Rflink messages as json encoded payload over MQTT OTA updates of the Wemos D1 mini Leds indicators for Wifi, Mqtt, message in, message out Simple http interface to view Rflink and Mqtt messages EDIT on 03.01.2023 : the First version was using SoftwareSerial on D1 and D2 to communicate with RFlink. But at 57600 bauds, from time to time some serial characters were lost and / or trashed. So I updated the code and the hardware to use SoftwareSerial for serial debugging and the hardware serial port is now used for the Rflink communication through D7 and D8 (using the swap() function). I updated the text of this post, but not the photos.\nEDIT on 12.02.2024 : I released a new version with a better web interface and a reset feature. The pin D1 (gpio5) is no more used for led feedback. It can now be wired to the arduino\u0026rsquo;s RST pin via a 100 ohm resistor to automatically reset Rflink when no message has been received after a few minutes (usualy, this is because Rflink has crashed). Again, I updated the text of this post, but not the photos.\nThe updated source code is available on github : https://github.com/jit06/RflinkToJsonMqtt\nThe new 3d enclosure can be found on Thingiverse : https://www.thingiverse.com/thing:5415688\nHardware build Used parts Arduino mega Pro : rflink works only on Mega hardware Wemos D1 mini : any esp8266 should do the trick SRX882 and STX882 kit with antennas : any other combinaison supported by Rflink will work Multicolor LEDs breadboard found on ebay Some wires / solder / soldering iron Wiring tables SRX882 Arduino Mega Pro Comments VCC 3v3 power for SRX882 GND GND ground Data D19 as seen on rflink instructions STX882 Arduino Mega Pro Comments VCC D15 GND GND linked with SRX882 Data D14 as seen on rflink instructions Wemos D1 mini Arduino Mega Pro Comments 5V Voltage regulator pin see below GND GND ground D7 TX serial connection (EDIT – was connected to D1 in the previous version) D4 RX serial connection (EDIT – was connected to D2 in the previous version) D1 RST (via 100 ohm resistor) Trigger reset signal (EDIT – was connected Led 2 in the previous version). The resistor is used to make a voltage divider which makes the voltage low enough on RST to trigger a reset of the Arduino Wemos D1 mini Leds breadboard Comments D0 Led 1 – green Wifi signal : blink while trying to connect, stay on if connected D2 Led 2 – blue Mqtt connection : blink while trying to connect, stay on if connected D5 Led 3 – white Blink when RF order is received and converted to mqtt message D6 Led 4 – orange Blink when RF order is sent Build steps No real complexity here : I mainly followed the rflink documentation and put in common some VCC and GND connections. Through, note that the ESP8266 is powered via one of the Arduino Pro voltage regulator. The Wemos D1 accepts to be powered through the 5v pin only if the voltage is between 4.3 and 4.6v.\nWiring ground and vcc in common for SRX 882 + STX882 :\nWiring the two RF modules to the Arduino board\nAdding leds module to Wemos D1 mini and wiring to Arduino as a Serial reader / writer. Basicaly, the Wemos use the hardware serial interface swapped on D7 and D8 ( EDIT - in the previous version, it was using SoftwareSerial on D1 and D2) to communicate with the standard serial pins of the Arduino Mega\n3D printed enclosure The enclosure I made have been printed on a Anycubic I3 Mega in PLA. It tooks around 2 hours. The sources of the model (Freecad 0.19) are available on Thingiverse together the STL files.\nThe enclosure is a bit tight so everything can be fixed without any skrew, by simply pushing it gently:\nSoftware part The original code as been modified in several ways :\nAdapted to run on ESP8266 with Wifi instead of Ethernet shield Added Leds handling HTTP server to show RF activity using websocket connections OTA update from Arduino IDE Refactored the main sketch to separate functionnalities in different files The source code is available on my github and can be compiled with Arduino IDE (check Readme.md to install the dependencies).\nUsage Example with OpenHab 3 Since the previous article in 2017, I upgraded from Openhab 2 to OpenHab 3 which has a new way to handle messages.\nHere is 2 examples of how I use my rflink2mqtt gateway.\nUsing Oregon OS-THGR228-N-THGR122 Create a channel with mqtt state topic : \u0026ldquo;rflink/Oregon_TempHygro/2DD1\u0026rdquo; (your ID will be different) Create an item for each properties, link them to the channel, use JSONPATH profile with the following expression : $.BAT for battery status $.HUM for humidity $.TEMP for temperature Controlling a Chacon IO switch Create a channel configured like the following : MQTT state Topic : empty MQTT Command Topic : \u0026ldquo;rflink/Order\u0026rdquo; Custom On/Open Value : \u0026ldquo;10;NewKaku;0000800;1;ON;\u0026rdquo; (your id will be different) Custom Off/Closed Value: \u0026ldquo;10;NewKaku;0000800;1;OFF;\u0026rdquo; (your id will be different) Link a switch Item to your channel As you can see, the gateway allows to write rflink orders directly to mqtt channel \u0026ldquo;rflink/Order\u0026rdquo; : the string is passed to Rflink.\n","permalink":"https://www.bluemind.org/rflink-mqtt-v2-enhanced-minimized/","summary":"\u003cp\u003eSome years ago, I built a \u003ca href=\"/custom-arduino-shield-mqtt-rflink/\"\u003ecustom shield for an arduino mega\u003c/a\u003e running Rflink. The purpose was to \u003ca href=\"images/020971d840c9632d892276977cb203a2d9a5b73c_original.jpeg\"\u003e\u003cimg\n  src=\"images/020971d840c9632d892276977cb203a2d9a5b73c_original.jpeg\"\n  alt=\"Arduino Mega Pro board\"\n  class=\"article-image\"\n  loading=\"lazy\"\n\u003e\u003c/a\u003e provide Rflink MQTT messages over ethernet. I recently built a new version which is more compact and with additional features.\u003c/p\u003e\n\u003cp\u003eI replaced the arduino nano by a Wemos D1 mini (esp8266). I also used an arduino mega pro, which is smaller than the orignal one. The 433Mhz hardware has been changed to by SRX 882 and STX882 as they provide a better range than RXB6 and XD-FST FS1000A.\u003c/p\u003e","title":"RFLink-to-MQTT Gateway with ESP8266 and Arduino Mega"},{"content":"I recentlly upgraded my internet connection from 100 megabits to 1 gigabit. With such a high bandwidth, I started to see the limits of my PFSense router, a PCEngine APU2. I live in Europe and since July 2021, buying product fom china is limited to 150 euros if you don\u0026rsquo;t have a company. Unfortunately, finding a gigabit capable PFsense based router is that not easy for such a low budget, and at the time (sept 2021), there were no affordable offer on local shops nor on Amazon.\nActualy after some research, I found a pretty good hardware for cheap !\nThe problem [caption id=\u0026ldquo;attachment_2252\u0026rdquo; align=\u0026ldquo;alignright\u0026rdquo; width=\u0026ldquo;300\u0026rdquo;] Pcengine APU2c4[/caption]\nMy initial router was a PcEngine apu2c4 which had 4 Gb of RAM, an AMD GX-412TC cpu running at 1Ghz. This hardware is really capable and I was running 4 vlans, more than 50 firewall rules, suricata and some OpenVPN connections from time to time.\nWhen my internet connection has been upgraded to 1 gigabit, I was not able to saturate the link. My Download speed was maxing out at 570 Mbps and the upload was reaching aroung 680 / 700 Mbps. During such high transfert rates, the CPU usage was 100% and this was with only one machine on the lan.\nSo I wanted to upgrade the hardware. I know that linux could perform a little bit better that PFSence (FreeBSD) , but I wanted to keep PFSense.\nFinding the proper hardware CPU being the bottle neck, I had to find a router with a better CPU, around 2 times faster. Pretty easy, just check www.cpubenchmark.net \u0026hellip; Hmmm well not really.\nI had also other requirements :\nAt least 3 nics (wan + 2 lan) 4Gb of ram or more than 16 Gb disk (more on that below) Fanless (silent) Maximum 4 centimeters height so I could insert the router in a 1U rack Console on Serial port (no display on my rack) The Right CPU There are some constraints regarding the cpu : Row power over 1065 CPU mark and single thread rating over 412 (see AMD GX-412HC benchmark) Low power consumption: the router runs 24/7, it is not conceivable to runs a standard desktop CPU (plus, remember the fanless requirement) 4 cores : for better multi-tasking (vlans, mutltiple connections, suricata, VPN, web interface, ntopng\u0026hellip;) AES-NI for hardware accelerated crypto : it\u0026rsquo;s mandatory for good performance with VPN connections Now, looking at available routers, we usualy see the following CPUs:\nCore i5 gen 7 to 10 : usualy the price is too high (x2 regarding a celeron) Core i7 gen 7 to 10 : same as i5 CPUs Celeron 4205U : only 2 cores and cpu mark to low (1321) Celeron J1900 : not powerfull enough (1136) Celeron J4125 : pretty good candidate, 4 cores and very nice performance (3041) Celeron N2940 : not powerfull enough (1018) Celeron 3865U : not powerfull enough (1225) Pentium 6405U : good candidate, only 2 cores but 4 threads and performance seems to be fine (2360) This is, of course, not an exaustive list, but cheapest hardware tend to have one of theses CPUs. So I had 2 candidates : Pentium 6405U or ideally Celeron J4125.\nRAM and storage With the APU2C4, I had a small 16 Gb ssd. So in order reduce wear, I configured a big ramdisk pace (2 Gb). 1Gb should be enough, but using suricata with ramdisk need at least 800 Mb in addition to download and uncompress rules.\nWith such a large ramdisk, I was using around 75% of the ram and I was not able to safely activate ntopng.\nSo all in all, the need is either a bigger storage space or more ram\u0026hellip; or both ;)\nAlso, ram and ssd could be purchased in a different package to be under the limit of 150 euros. This is another constraint : finding an appliance which is sold without ram and storage.\nResult : the ideal config Unboxing and boot I finally found a quite good mini PC under the reference \u0026ldquo;bkhd g40\u0026rdquo; on aliexpress :\nMay be bought with no ram and no storage for 140 euros Available with the Celeron J4125 !! Has support for standard msata ssd Two DDR4 slots for a maximum of 8 Gb ram 4x gigabit intel i211, very well supported by PfSense Less than 4 centimeters height The only missing thing is an external serial port, but a COM port is available on the motherboard, so it is easy to plug a floating rs232 port.\nHere is the product as I received it :\nInside :\nWith 2x4 Gb ddr4 and 256 Gb storage : plenty of ressources for PfSense !\nBooting to the BIOS :\nAdding an exteral serial port I used a RS232 slot plate bracket (brand is \u0026ldquo;startech\u0026rdquo;) and 3 dupont wires (male - female).\nI juste wired the female dupont wire to the motherboard\u0026rsquo;s com port, so I could simple get an RS232 using the following schema (RX to TX, TX to RX, ground)\nLive, with a RS232 to USB cable :\nConclusion For a total price of about 250 euros, I have a now a very powerfull PfSense router which fullfill all my needs :\nCan saturate my gigabit connection in both upload and download without saturating the CPU Additional services like surricata, ntopng, haproxy All logs to disk with no ramdisk Compact and fanless Hopefully future proof regarding row power ! ","permalink":"https://www.bluemind.org/2249-2/","summary":"\u003cp\u003eI recentlly upgraded my internet connection from 100 megabits to 1 gigabit. With such a high bandwidth, I started to see the limits of my PFSense router, a \u003ca href=\"https://pcengines.ch/apu2c4.htm\"\u003ePCEngine APU2\u003c/a\u003e.\nI live in Europe and since July 2021, buying product fom china is limited to 150 euros if you don\u0026rsquo;t have a company. Unfortunately, finding a gigabit capable PFsense based router is that not easy for such a low budget, and at the time (sept 2021), there were no affordable offer on local shops nor on Amazon.\u003c/p\u003e","title":"Fanless pfSense Router for Gigabit WAN"},{"content":"I recently finished the V3.0 of my custom retroarch build distribution for oddroid C1 : https://github.com/jit06/retroarch_fbdev_c1.\nAmongst other things, the are 2 main features that pushed me to upgrade my homemade console, the GamOdroid C0 :\nUsb sound card support Gpio joypad integration (https://github.com/jit06/gpio_joypad) Comparing to the original Debian based linux, the console now boots faster (14 sec), has better performances for Dreamcast and N64 emulation, has a on-screen battery level monitor and adjustable brightness and contrast (start + left/righ and start + up/down).\nThe following photos show the console in action. The images in the bottom left corner that some photos show are actually video previews of the selected game (start button trigger the video, as explained on the project\u0026rsquo;s wiki\n","permalink":"https://www.bluemind.org/gamodroid-c0-updated-with-fbdev-retroarch/","summary":"\u003cp\u003eI recently finished the V3.0 of my custom retroarch build distribution for oddroid C1 : \u003ca href=\"https://github.com/jit06/retroarch_fbdev_c1\"\u003ehttps://github.com/jit06/retroarch_fbdev_c1.\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eAmongst other things, the are 2 main features that pushed me to upgrade my homemade console, \u003ca href=\"/gamodroid-c0-odroid-based-portable-retrogaming/\"\u003ethe GamOdroid C0\u003c/a\u003e :\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eUsb sound card support\u003c/li\u003e\n\u003cli\u003eGpio joypad integration (\u003ca href=\"https://github.com/jit06/gpio_joypad\"\u003ehttps://github.com/jit06/gpio_joypad\u003c/a\u003e)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eComparing to the original Debian based linux, the console now boots faster (14 sec), has better performances for Dreamcast and N64 emulation, has a on-screen battery level monitor and adjustable brightness and contrast (start + left/righ and start + up/down).\u003c/p\u003e","title":"GamOdroid C0 updated with fbdev retroarch"},{"content":"The idea emerged from a fantastic Thingiverse project http://thingiverse.com/thing:2700850. I\u0026rsquo;m fan of RC hovercraft since my childhood, but it seems that we are too few in that case to have good industrial response ;)\nI did not create anything here, but I found it not that easy to find a true complete requirement list and what parts from original project\u0026rsquo;s remixes I should take.\nSo this article is the story of my build with, I hope, enough informations to make this fun project more accessible !\nRequirements 3d print parts I used part from the original project and the following remixes:\nhttp://thingiverse.com/thing:4158803 http://thingiverse.com/thing:2719984 I printed all parts in PLA with my Anycubic i3 Mega (20x20 print surface): 0.4 mm nozzle first layer 0.3 layer height 0.2 15% infill supports everywhere on all but the lower hull, skirt ring and skirt clips print speed : 40 Note that the biggest part must be printed in diagonal on the print bed. Cumulated total print time is around 75 hours, with the longest part to print that took 13h40.\nThe lower hull, upper hull and thrust duct have been glued with with Araldite Standard (epoxy based glue). This is a really good glue for this king of job, as it takes severals hours to fix, so you have time to adjust the parts.\nElectronic parts This is were it can be difficult to understand what is exhaustively needed if you are not familiar with R/C. You will find below all parts I really used, where I found them and their cost in euros : Items Where Price Brass tube, 3mm inside hole diameter amazon 10 2 little bee 30A ESC. My advice is to buy 3 if one is defective aliexpress 12 2 brushless RS2205 2300kv. I took a pair with one clockwise and the other counter-clockwise, just pair them with corresponding propellers aliexpress 12 2 propellers, clockwise or counter clockwise, depending on your motors. I bought a kit of 12 in various colors to be able to choose the color I prefer during the build. aliexpress 9 One connecting rod, 1.5 mm. I bought a kit with 5 pieces, just in case 😉 aliexpress 3 2.4 Ghz Microzone controller and receiver kit, 6 channels (only 3 channels needed). I took a right handed one so the lift motor can be triggered with the right thumb, while the left thumb is used to accelerate. aliexpress 28 A Power distribution board, Matek XT60 3A aliexpress 5 A 9g Emax micro servo (ES08A) aliexpress 6 Imax B6 lipo charger, complete pack aliexpress 27 8 M3x12 skrew. I bought anodised ones in the same colors as the motor (red) aliexpress 4 1.3 mm connectors plug for connecting rods. Only one is needed, but there are often sold in pack aliexpress 5 At least one 3S lipo battery. I personnaly bought 3 pieces : Turnigy 2200mAh 3S 25C hobbyking 12 TOTAL 133 The build Step by step Glue the two main parts and check that the trust duct can be screwed\nMount the thrust duct propeller and check how it fits\nCut small brass tube parts an put them in the holes before skrewing the rudders\nAdd the tranversal bar at the bottom of the rudders so they move together. I fixed them with standard RC clips. Also add the connector plug that will be used for the connecting rod.\nMake 4 holes in the left rear part, just before the thrust duct, to mount the power distribution board. I skrewed it from the bottom of the body.\nThe lift propeller enclosure is not big enough for the 5045 propeller, so the propeller must be cut to fit.\nThen solder the ESC, and fix it under the body\u0026rsquo;s surface with some double sided adhesive.\nMount the Servo on the right, put the receiver on the middle of the thrust duct fixation legs, and fix the thrust propeller\u0026rsquo;s ESC on one of the two middle fixation legs of the thrust duct (again, with double sided adhesive)\nCut the skirt. Here I used an old raincoat, but a good trash bag works also (it\u0026rsquo;s just less robust). As you can see, I used the inside of the raincoat (a kind of grid) to limit the number of smalls pebbles that could enter inside.\nFinaly, fix the skirt, lift motor grid and add a battery\u0026hellip; It\u0026rsquo;s ready to go !\nLesson\u0026rsquo;s learnt As every build I saw, the main concern is the skirt. Even the rain coat is starting to show some strong signs of wear after 5 or 6 hours of use. I need to find something stronger\u0026hellip; The 2200 mAh battery is big and does not look so good in the hovercraft, but it last 15 to 20 minutes ! Even with the protection I put under the body (the king of grid), there are some small pebbles that enter during a session. Though, there are easy to remove. Hovercraft in action ","permalink":"https://www.bluemind.org/3d-printed-rc-hovercraft/","summary":"\u003cp\u003eThe idea emerged from a fantastic Thingiverse project \u003ca href=\"http://thingiverse.com/thing:2700850\"\u003ehttp://thingiverse.com/thing:2700850\u003c/a\u003e. I\u0026rsquo;m fan of RC hovercraft since my childhood, but it seems that we are too few in that case to have good industrial response ;)\u003c/p\u003e\n\u003cp\u003eI did not create anything here, but I found it not that easy to find a true complete requirement list and what parts from original project\u0026rsquo;s remixes I should take.\u003c/p\u003e\n\u003cp\u003eSo this article is the story of my build with, I hope, enough informations to make this fun project more accessible !\u003c/p\u003e","title":"3d printed RC Hovercraft"},{"content":"After several months of usage, I wanted to correct some problems and \u0026ldquo;polish\u0026rdquo; a little bit my homemade audiophile music tablet (see the build story here)\u0026hellip;\nHardware modification The problem was that the screen drawn too much power from USB port while the C0 was powered through battery. Without a power source connected, the screen was sometime blinking.\nThe solution was to use a more powerfull external battery charger / booster: the lipo rider plus , which support 2.4A. It now powers the Odroid C0 and the display directly.\nThe modifications can be seen in the photo below : the lipo rider plus is in the center. Note that I removed the leds that was used to check battery level, as they are no more used. I kept the wires, just in case I would like to add something ;)\nSoftware modification This is where the \u0026ldquo;more polished things\u0026rdquo; are visible. The OS is now based on my own build of Volumio which is itselft based on my own Ubuntu tiny distribution with my own kernel build.\nWhat has changed :\nBoot splash logo during boot Vumeter leds show boot progression The embeded chromium is now GPU accelerated using GLES\u0026hellip; and it\u0026rsquo;s a lot smoother ! The touch button actions are now handled by a volumio plugin I wrote : \u0026ldquo;gpiorandom\u0026rdquo; Vumeter engine including Cava is started and stopped depending on volumio status, instead of being always running. For that, I use another volumio plugin I wrote : \u0026ldquo;commandOnEvent\u0026rdquo;. The ready to boot Odrophile image can be found in my github : https://github.com/jit06/odrophile\nYou can also found :\nThe kernel : https://github.com/jit06/linux/releases The tiny ubuntu distribution : https://github.com/jit06/tiny-ubuntu The custom volumio build : https://github.com/jit06/Build Volumio plugins have been integrated in the official build. In action ","permalink":"https://www.bluemind.org/odrophile-update/","summary":"\u003cp\u003eAfter several months of usage, I wanted to correct some problems and \u0026ldquo;polish\u0026rdquo; a little bit my homemade audiophile music tablet (\u003ca href=\"/odrophile-odroid-c0-based-audiophile-tablet/\"\u003esee the build story here\u003c/a\u003e)\u0026hellip;\u003c/p\u003e\n\u003ch2 id=\"hardware-modification\"\u003eHardware modification\u003c/h2\u003e\n\u003cp\u003eThe problem was that the screen drawn too much power from USB port while the C0 was powered through battery. Without a power source connected, the screen was sometime blinking.\u003c/p\u003e\n\u003cp\u003eThe solution was to use a more powerfull external battery charger / booster: \u003ca href=\"https://wiki.seeedstudio.com/Lipo-Rider-Plus/\"\u003ethe lipo rider plus\u003c/a\u003e , which support 2.4A. It now powers the Odroid C0 and the display directly.\u003c/p\u003e","title":"Odrophile : update !"},{"content":"The Raspberry pi has some very high quality DAC such as the Pecan Pi. But regarding integration, even if there are existing cases, I was missing a remote, some basic physical user interface and a CD player.\nThis is the story of this build.\nMain Features 24 bits / 192 Khz DAC Balanced XLR output 2 front USB ports Volume knob 1 action button (play/pause, play radio, random playlist) 1 status led Remote control 100 Mbits Ethernet Slot-in CD player Runs Volumio Used Components PecanPi Streamer V1.5 (Raspberry pi 3b + PecanPi DAC), full review here 16 Gb MicroSD class 10 1 x IR receiver Led A remote controller from an old CD player An old PCI Ethernet Card 2 x USB mal connectors breakout boards 4 x Metal spacers Small ethernet cable with an rj45 connector Hitachi-LG slot-in DVD drive (model GS40N) Streacom ST-F7CB Evo PC case Usb3 to sata converter Hardware build Fix the Pi and the DAC into the case The two boards as been centered in the case. The screw supports have been glued.\nFront connectors and Leds The front USB ports have a standard USB3 ATX connector. I used 2 usb breakout boards and some dupont cables to link the front USB ports to Raspberry Pi 3 ones.\nThe case have an IR window on the front face, so I fixed the IR led just behind. It has been wired to GPIO 25 and ground, on pins 22 and 20\nLike all PC cases, there is a front led. I wired it to GPIO 12 and ground, on pins 32 and 34 (there is also a resistor to the ground).\nI also make a hole in the case to fix the volume knob that comes with the PecanPi..\nEthernet connector In order to have the audio connectors on the rear face, the ethernet connector of the Raspberry Pi is not easily accessible. I used an old ethernet card to cut it\u0026rsquo;s connector and wire it to a small rj45 plugged into the Pi. I could have use any female to female connector, but I wanted to use the PCI support to provide a strong support.\nCD Player The CD Player is simply attached normaly to the case and plugged to the Pi via an USB to SATA connector.\nFinal result The back panel plate has been 3D printed using a universal IO Shield project form thingiverse: https://www.thingiverse.com/thing:3076355\nSoftware part The device is driven by Volumio, but I had to write some plugins and provides an Lirc definition for my remote in order to have all functionalities All the plugins have been integrated in Volumio via a pool request and are accessible from it\u0026rsquo;s interface.\nLedstatus Original sources : https://github.com/volumio/volumio-plugins/tree/master/plugins/user_interface/ledstatus\nBlink a GPIO wired led while playing. I used this to show play status throught the case front led.\nStatus2mqtt Original sources : https://github.com/volumio/volumio-plugins/tree/master/plugins/user_interface/status2mqtt\nThis plugin send the volumio status to an mqtt server when it changes. I use it to auto power on / off my speakers (Eve audio SC205 + TS108) with a chacon IO receiver.\nGpiorandom Original sources : https://github.com/volumio/volumio-plugins/tree/master/plugins/user_interface/gpiorandom\nThis one allows to assign an action to a button wired to one GPIO. You can set an action for single, double and triple click. Actions can be :\nToggle play / pause Play one of your favorites radios Play a given URI Build a playlist with random songs and play it IR Remote Controller For this one I just created an LIRC profile for my remote (Philips CD723). The profile has been integrated in the main volumio branch.\n","permalink":"https://www.bluemind.org/diy-volumio-driven-audiophile-dac-and-cd-player/","summary":"\u003cp\u003eThe Raspberry pi has some very high quality DAC such as the Pecan Pi. But regarding integration, even if there are existing cases, I was missing a remote, some basic physical user interface and a CD player.\u003c/p\u003e\n\u003cp\u003eThis is the story of this build.\u003c/p\u003e\n\u003ch2 id=\"main-features\"\u003eMain Features\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e24 bits / 192 Khz DAC\u003c/li\u003e\n\u003cli\u003eBalanced XLR output\u003c/li\u003e\n\u003cli\u003e2 front USB ports\u003c/li\u003e\n\u003cli\u003eVolume knob\u003c/li\u003e\n\u003cli\u003e1 action button (play/pause, play radio, random playlist)\u003c/li\u003e\n\u003cli\u003e1 status led\u003c/li\u003e\n\u003cli\u003eRemote control\u003c/li\u003e\n\u003cli\u003e100 Mbits Ethernet\u003c/li\u003e\n\u003cli\u003eSlot-in CD player\u003c/li\u003e\n\u003cli\u003eRuns Volumio\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"used-components\"\u003eUsed Components\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://orchardaudio.com/shop/ols/products/pcnp-strmr\"\u003ePecanPi Streamer\u003c/a\u003e V1.5 (Raspberry pi 3b + PecanPi DAC), \u003ca href=\"https://www.audiosciencereview.com/forum/index.php?threads/pecanpi-next-generation-raspberry-pi-dac-and-streamer.6952/\"\u003efull review here\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e16 Gb MicroSD class 10 \u003ca href=\"images/20200809_125833-rotated-e1616711934915.jpg\"\u003e\u003cimg\n  src=\"images/20200809_125833-rotated-e1616711934915.jpg\"\n  alt=\"\"\n  class=\"article-image\"\n  loading=\"lazy\"\n\u003e\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e1 x IR receiver Led\u003c/li\u003e\n\u003cli\u003eA remote controller from an old CD player\u003c/li\u003e\n\u003cli\u003eAn old PCI Ethernet Card\u003c/li\u003e\n\u003cli\u003e2 x USB mal connectors breakout boards\u003c/li\u003e\n\u003cli\u003e4 x Metal spacers\u003c/li\u003e\n\u003cli\u003eSmall ethernet cable with an rj45 connector\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"http://www.bluraysupplier.com/fr/products/HL-GS40N-Slot-in-SATA-Internal-DVD-Burner.html\"\u003eHitachi-LG slot-in DVD drive (model GS40N)\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://streacom.com/products/f7c-evo-chassis/\"\u003eStreacom ST-F7CB Evo PC case\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.delock.de/produkte/1004_USB-vers-SATA/64048/merkmale.html\"\u003eUsb3 to sata converter\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"hardware-build\"\u003eHardware build\u003c/h2\u003e\n\u003ch3 id=\"fix-the-pi-and-the-dac-into-the-case\"\u003eFix the Pi and the DAC into the case\u003c/h3\u003e\n\u003cp\u003eThe two boards as been centered in the case. The screw supports have been glued.\u003c/p\u003e","title":"DIY volumio driven audiophile DAC and CD player"},{"content":"The Odroid C0 is definitly one of my favourites DIY plateform : RTC, lipo support, emmc, fast cpu / gpu and low power consumption ! This time, I made an \u0026ldquo;audiophile\u0026rdquo; tablet with a neo-retro look, based on a C0 and the Hifi shield plus.\nFeatures Hardware 7 inch capacitive touch display High quality I2S DAC Audio out : mini stereo jack, RCA and Toslink Led vu-meter with 7 level for each chanel Rotary potentiometer used to switch on/off the tablet and control the volume The rotary potentiometer is also touch sensitive 6000 mah lipo battery battery meter 1x USB 2.0 port 1x micro usb port for charging 2x ps3 copper pad Software The tablet runs a slightly tuned Volumio image to make it boots faster and to manage specific functionalities such as the touch sensitive potentiometer, vu-meter and battery monitor.\nUsed components Odroid C0 Hifi shield plus Generic 7inch HDMI touchscreen HDMI fpv cable with one 90 degree connector No-name Lipo battery from ebay (model ref 906090 on ebay) 2x breadboards (4cm x 6cm) 14x 3mm leds (8 green, 4 yellow, 2 red) 1x female USB connector 1x micro usb breakout 1x CR2032 battery holder 1x rotary potentiometer with on / off switch (with a button) 1x capacitive touch breakout with pad pin 1x stereo jack (3.5\u0026quot;) 1x stereo jack female connector Some dupont cables 6bit Multicolor LED breadboard Some resistors (8x 1k, 4x 4.7k, 2x 10k. 1x 1M) Some pin rails Hardware build My goal was to build something which I could repair in time, or change some parts in case of failure. That\u0026rsquo;s why I used pin rails and dupont cables instead of direct soldering.\nOdroid preparation The Odroid C0 is nude by default : no connector, no pin rail. This allows to easily arrange connectors as you want.\nI started to put 90 degrees pin rails for usb ports and j7 headers as well as a standard 40 pins rails on the GPIO port:\nI removed the on/off switch to wire the volume knob that also have a on/off switch. Note the 1 M ohm resistor so the odroid can be switched on / off with a simple 2 wires switch (thanks to Odroid support : https://forum.odroid.com/viewtopic.php?f=111\u0026amp;t=39899):\nFinally, I also twisted a little bit the hifi shield connector and added the 2 copper pads with some thermal paste (fixed with meccano parts):\nDisplay The display has two micro-usb ports, but there are linked on the same tracks. As theses connectors are very small to solder wires on it, I took +5v and D- on the upper connector and the D+ and GND on the lower connector. I used dupont cable to be able to connect the display to the usb port pins shown previously:\nIn order to securise the connection I put some hot glue and a piece of adhesive tape:\nConnectors The goal here was to build a kind of \u0026ldquo;daughter board\u0026rdquo; with main connectors : power, audio jack, one usb port and the battery clock. All connectors are mounted and soldered on a 4x6cm breadboard.\nIn order: place all connectors and solder them on the back. Then add audio jack, power cable and CR2032 battery holder; cables are also fixed with hot glue.\nThe USB connector is linked to a 4 pins rail which will be connected to one of the odroid C0 usb ports.\nThe audio jack will be connected to the jack output of the hifi shield because the jack connector on it is not on the same side as other audio out connectors.\nVu-meter and battery monitor Like the connectors\u0026rsquo; \u0026ldquo;daughter board\u0026rdquo;, the base for vu-meter leds is a 3x6cm breadboard, but this time, cut it in two parts. Each part contains 7 leds with their associated resistors. This provides 7 levels for left and right audio channels.\nI used a standard 40 pins ribbon cable (IDE / PATA) to make it easier to connect all leds to GPIO ports.\nI wired the same way the 6 bits led board to display the battery level on the right side of the tablet:\nTouch sensitive potentiometer The potentiometer has also been wired to the GPIO through the ribon cable:\nIn order to make it touch sensitive, I used a breakout from Sparkfun. You may find any other brand, but it is important to get one with a \u0026ldquo;pad\u0026rdquo; pin. The idea is to use this pin and not the touch part which is hidden in the middle of the tablet. The pad pin is wired to the rotative part of the volume knob which is all metal and so, become touch sensitive.\nGPIO wiring and first tests The following table show the wiring plan I used. Note that to optimise space inside the tablet, the 40 pins ribbon cable connector has been plugged in reverse order regarding GPIO pin. So the GPIO pin 1 correspond to the ribbon cable pin 40.\nBelow if a first test putting all together to check everything before the final assembly in the tablet case, except the display (I used a standard hdmi monitor for this first test)\u0026hellip; hopefully no smoke, the Odroid C0 started correctly !\n3D print and assembly I modelized the case in Freecad. All files are availables on thingiverse : https://www.thingiverse.com/thing:4641846\nThe skrew supports are filled and need to be drilled. It is more robust and give more liberty regarding the skrew types.\nThe 3D printed case, painted with white satin spray, for better rendering than \u0026ldquo;nude\u0026rdquo; filament:\nAssembly of the front part :\nAssembly of the back :\nGlobal view before closing the case :\nSoftware part The idea was to start from the base Volumio image for odroid C1 and a maximum of functionalities through shell scripts to avoid dependencies on the image (it uses an old Debian as base os). I used the version 2.834 (24.09.2020) : https://volumio.org/get-started/\nAll scripts I used can be found on my github : https://github.com/jit06/odrophile Shell scripts have been stored in /opt and called form /etc/rc.local:\n/opt/gpio_init.sh /opt/vu_meter.sh \u0026amp; /opt/adc_volume.sh \u0026amp; /opt/batterymon.sh \u0026amp; /opt/touchbutton.sh \u0026amp; Base install Boot the image then :\nInstall the \u0026ldquo;touch display\u0026rdquo; plugin (in category \u0026ldquo;Miscellanea\u0026rdquo;) Activate ssh by visiting http://volumio.lan/dev Replace boot.ini by boot.ini.vu7+ (rename) In boot.ini, comment out \u0026ldquo;disableuhs\u0026rdquo; if your microsd support it Finaly, reboot : after a while, the screen now show Volumio interface on the touchscreen.\nSetting up volumio I used the following settings in volumio.\nin \u0026ldquo;Sources\u0026rdquo;:\nDisable upnp, shairport-sync, dlna-browser Enable \u0026ldquo;show track number\u0026rdquo; Hide : albums, mediaservers, webradio , music library in \u0026ldquo;Playback\u0026rdquo;:\nOutput device : hifi shield Upsampling: 24/192 in \u0026ldquo;Appearance\u0026rdquo;:\nChange to classic interface (more suitable for small screen) in \u0026ldquo;System settings\u0026rdquo;:\nStartup sound : off Allow UI statistics : off Optimize the boot time By default., boot time is terrible. Here is what I changed to speed it up a bit.\nDisable unneeded services (your needs may vary) :\nsystemctl disable winbind nmbd samba-ad-dc smbd bfs-common lirc nfs-common plymouth-quit bluetooth cd shairport-sync The Volumio system put the c1_init call in /etc/rc.local instead of putting it in a hook in mkinitcpio. As this script take some time to execute because of the framebuffer initialisation, we can remove its dependency in systemd, so it starts early in boot process, in parallel of all other services.\nIn file /lib/systemd/system/rc-local.service comment the network dependencies :\n#After=network.target Then, reload daemon :\nsystemctl daemon-reload Another thing to remove : pulseaudio as it is not used.\napt-get remove pulseaudio sudo rm /etc/xdg/autostart/pulseaudio* We also need to black list some kernel modules to free up GPIO pins and remove unused features. Here is the content of the /etc/modprobe.d/blacklist.conf file I used:\nblacklist nfsd blacklist auth_rpcgss blacklist sx865x blacklist oid_registry blacklist nfs_acl blacklist nfs blacklist lockd blacklist sunrpc blacklist w1_gpio blacklist wire Finaly, I removed aml_i2c module as it generates a lot a errors and is not used. Unfortunately, putting the module in the black list does not work, so I inserted a line in /etc/rc.local, just before \u0026ldquo;exit 0\u0026rdquo;:\nrmmod aml_i2c Leds vu-meter The leds vu-meter is based on two parts. One is the \u0026ldquo;backend\u0026rdquo;, which analyses mpd output and generates sound level informations in ascii numbers from 0 to 7. The second part is the \u0026ldquo;frontend\u0026rdquo;: it reads the output in realtime with a shell script that switches ON or OFF each led by driving the GPIO pins output.\nThe backend The backend tool is Cava, a marvelous console-based audio visualizer. It had to be built from sources :\nsudo apt-get update sudo apt-get install -y git-core autoconf make libtool libfftw3-dev libasound2-dev inotify-tools git clone --depth 1 https://github.com/karlstav/cava cd cava ./autogen.sh ./configure make -j4 sudo make install Cava does its job by reading a fifo. So we need to create a mpd fifo. To do so, I modified the template that Volumio uses, so the settings remain evan if parameters are changed in the Volumio interface.\nIn /volumio/app/plugins/music_service/mpd/mpd.conf.tmpl :\naudio_output { type \u0026#34;fifo\u0026#34; name \u0026#34;mpd_oled_FIFO\u0026#34; path \u0026#34;/tmp/mpd_oled_fifo\u0026#34; format \u0026#34;44100:16:2\u0026#34; } Cava needs some tuning for our use case. In /home/volumio/.config/cava/config, I set the following. Note that I removed all non modified content in this blog post, but in reality, I kept the original content and I juste changed the parameters like below:\nframerate = 30 # the more fps, the more cpu usage autosens = 1 bars = 2 # one for left, one for right method = fifo # read from the fifo source = /tmp/mpd_oled_fifo # the fifo in which mpd output goes sample_rate = 44100 # same as in mpd fifo configuration sample_bits = 16 # same as in mpd fifo configuration method = raw channels = stereo raw_target = /dev/stdout # output to stdout so we can pipe to our \u0026#34;front-end\u0026#34; script data_format = ascii bit_format = 16bit ascii_max_range = 7 # 7 leds per channel gravity = 200 # makes vu-meter more dynamic The frontend The shell script used to read the Cava output is on github : https://github.com/jit06/odrophile/blob/main/leds_vumeter.sh It uses the standard /sys/class/gpio interface to drive right and left vu-meters.\nThe vu-meter system is launched by a simple script in /etc/rc.local : https://github.com/jit06/odrophile/blob/main/vu_meter.sh\nVolume Knob Volume though ADC Handling volume trough Volumio seems bugged. I tried to activate \u0026ldquo;software\u0026rdquo; volume in settings, but it crashes. So the solution was to control volume via mpd directly. The drawback is that the current volume is not reflected in the Volumio user interface.\nTo activate software volume in mpd (/volumio/app/plugins/music_service/mpd/mpd.conf.tmpl), add the following in \u0026ldquo;audio_output\u0026rdquo;:\nmixer_type \u0026#34;software\u0026#34; Then, go to Volumio UI, \u0026ldquo;playback\u0026rdquo; options, then click \u0026ldquo;save\u0026rdquo; in order to regenerate mpd config file.\nThe script that handle volume is, again, on github https://github.com/jit06/odrophile/blob/main/adc_volume.sh It reads ADC value from GPIO and changes volume using mpc cli.\nNote that as ADC values go from 0 to 1024, I simply divided the value by 10 to get the raw volume value to pass to mpc (which accept values between 0 and 100).\nTouch sensitive knob As mentioned previously, the Sparkfun\u0026rsquo;s touch pad is wired to the metal part of the potentiometer. The touch pad detect that the knob is touched and we can read the corresponding GPIO value (1 = touched, 0 = not touched).\nI implemented a script that detect double touch and tripple touch in order to trigger respectively toggle play / pause and random play of 25 music tracks.\nThis script is here https://github.com/jit06/odrophile/blob/main/touchbutton.sh It may seems a little heavy / complicated, but the problem is that inotify does not detect gpio change in /sys/class/gpio pseudo filesystem, even if the gpio edge is set to \u0026ldquo;both\u0026rdquo;. So I had to implement the double and tripple touch using some polling mechanism.\nThe play / pause toggle use the Volumio CLI, and the random play (tripple touch) uses a nodeJS script given by a community member : https://community.volumio.org/t/adding-random-tracks-to-queue-track-to-album-loader.\nThis nodeJS script needs a dependency to work. To install it, go to /opt and install the module :\nsudo npm install socket.io-client Final result He are two photos and one video of the tablet playing some music:\n","permalink":"https://www.bluemind.org/odrophile-odroid-c0-based-audiophile-tablet/","summary":"\u003cp\u003eThe \u003ca href=\"https://www.hardkernel.com/shop/odroid-c0/\"\u003eOdroid C0\u003c/a\u003e is definitly one of my favourites DIY plateform : RTC, lipo support, emmc, fast cpu / gpu and low power consumption ! This time, I made an \u0026ldquo;audiophile\u0026rdquo; tablet with a neo-retro look, based on a C0 and the \u003ca href=\"https://www.hardkernel.com/shop/hifi-shield-plus/\"\u003eHifi shield plus\u003c/a\u003e.\u003c/p\u003e\n\u003ch1 id=\"features\"\u003eFeatures\u003c/h1\u003e\n\u003ch2 id=\"hardware\"\u003eHardware\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://fr.aliexpress.com/item/4000375954941.html?spm=a2g0o.productlist.0.0.5fb32353QI1mk0\u0026amp;algo_pvid=b5f4c9a1-f954-4a2b-99b8-0cb5a4b45ef3\u0026amp;algo_expid=b5f4c9a1-f954-4a2b-99b8-0cb5a4b45ef3-19\u0026amp;btsid=0b0a187916044404883847455eedf2\u0026amp;ws_ab_test=searchweb0_0,searchweb201602_,searchweb201603_\"\u003e7 inch capacitive touch display\u003c/a\u003e \u003ca href=\"images/odroid-c0.png\"\u003e\u003cimg\n  src=\"images/odroid-c0.png\"\n  alt=\"\"\n  class=\"article-image\"\n  loading=\"lazy\"\n\u003e\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eHigh quality I2S DAC\u003c/li\u003e\n\u003cli\u003eAudio out : mini stereo jack, RCA and Toslink\u003c/li\u003e\n\u003cli\u003eLed vu-meter with 7 level for each chanel\u003c/li\u003e\n\u003cli\u003eRotary potentiometer used to switch on/off the tablet and control the volume\u003c/li\u003e\n\u003cli\u003eThe rotary potentiometer is also touch sensitive\u003c/li\u003e\n\u003cli\u003e6000 mah lipo battery \u003ca href=\"images/hifi-shield-plus.jpg\"\u003e\u003cimg\n  src=\"images/hifi-shield-plus.jpg\"\n  alt=\"\"\n  class=\"article-image\"\n  loading=\"lazy\"\n\u003e\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003ebattery meter\u003c/li\u003e\n\u003cli\u003e1x USB 2.0 port\u003c/li\u003e\n\u003cli\u003e1x micro usb port for charging\u003c/li\u003e\n\u003cli\u003e2x \u003ca href=\"https://www.amazon.com/Sony-Playstation-PS3-Copper-Heatsink-Yellow/dp/B007VCU2SK\"\u003eps3 copper pad\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"software\"\u003eSoftware\u003c/h2\u003e\n\u003cp\u003eThe tablet runs a slightly tuned \u003ca href=\"https://volumio.org/get-started/\"\u003eVolumio image\u003c/a\u003e to make it boots faster and to manage specific functionalities such as the touch sensitive potentiometer, vu-meter and battery monitor.\u003c/p\u003e","title":"Odrophile : odroid C0 based audiophile tablet"},{"content":"If you have an Anycubic I3 Mega for some months, you probably know the problem : the base plate that support the heated bed tends to bend, especially on the corners. This is probably due to the spring that are too strong, but also the to base plate which is probably too thin, has too much holes and lacks a fourth rail supports on the left side.\nNevertheless, this is something that you can correct either by using a thicker base plate or to reinforce it. I recently took the option to reinforce it with a piece if wood for less than\u0026hellip; 2 euros !\nThere is nothing complicated here : I used a 12 milimeters thick wood piece that I cut to the exact size of the original base plate. I then cut the angle so the springs has he same height to extend. Finally, I added two screws in each corner to make them more rigid. As a result, my base plate is now perfectly flat even with tightened screws.\nI took some photos presented below.\n","permalink":"https://www.bluemind.org/anycubic-i3-mega-more-rigid-bed-for-cheap/","summary":"\u003cp\u003eIf you have an Anycubic I3 Mega for some months, you probably know the problem : the base plate that support the heated bed tends to bend, especially on the corners. This is probably due to the spring that are too strong, but also the to base plate which is probably too thin, has too much holes and lacks a fourth rail supports on the left side.\u003c/p\u003e\n\u003cp\u003eNevertheless, this is something that you can correct either by using a thicker base plate or to reinforce it. I recently took the option to reinforce it with a piece if wood for less than\u0026hellip; 2 euros !\u003c/p\u003e","title":"Anycubic i3 mega : more rigid bed for cheap !"},{"content":"Back in 2015, I did the GameOdroid C0, a portable gaming device based on Odroid C0.\nIt was running X under debian, and to be honest I was not entirely satisfied of the software parts :\nbad vsync some garbage things on the bottom of the screen crash of emulationstation when exiting a game (I had a script to force kill and restart each time) slow n64 emulation slow dreamcast emulation with anoying noise So I recently built a new system : retroarch_fbdev_c1 (https://github.com/jit06/retroarch_fbdev_c1). It\u0026rsquo;s an Odroid C1/C0 optimized retroarch build scripts, based on RetroPie and hardkernel\u0026rsquo;s Ubuntu minimal image. It uses fbdev and allows to play confortably a lots of retro consoles including n64 and dreamcast. It provides a way to scrap roms with skyscraper and convert de result into retroarch compatible playlists and thumbnails It has been designed for lowres display (cbvs or 480p).\nAmong other things, it boots in 14 seconds from cold start to retroarch and display a splash screen during boot.\nThe latest binary release can be downloaded on the github repo: https://github.com/jit06/retroarch_fbdev_c1/releases\nI\u0026rsquo;ve sent patches and suggestions to Retropie project, so they may enhance Odroid C1/C1+/C0 support for lr-flycast, amiberry as well as the main build script: https://retropie.org.uk/forum/topic/251 \u0026hellip; 4893984159\n","permalink":"https://www.bluemind.org/odroid-c1-fbdev-retroarch-build-scripts/","summary":"\u003cp\u003eBack in 2015, I did the \u003ca href=\"/gamodroid-c0-odroid-based-portable-retrogaming/\"\u003eGameOdroid C0\u003c/a\u003e, a portable gaming device based on Odroid C0.\u003c/p\u003e\n\u003cp\u003eIt was running X under debian, and to be honest I was not entirely satisfied of the software parts :\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ebad vsync\u003c/li\u003e\n\u003cli\u003esome garbage things on the bottom of the screen\u003c/li\u003e\n\u003cli\u003ecrash of emulationstation when exiting a game (I had a script to force kill and restart each time)\u003c/li\u003e\n\u003cli\u003eslow n64 emulation\u003c/li\u003e\n\u003cli\u003eslow dreamcast emulation with anoying noise\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eSo I recently built a new system : retroarch_fbdev_c1 (\u003ca href=\"https://github.com/jit06/retroarch_fbdev_c1\"\u003ehttps://github.com/jit06/retroarch_fbdev_c1\u003c/a\u003e).\nIt\u0026rsquo;s an Odroid C1/C0 optimized retroarch build scripts, based on RetroPie and hardkernel\u0026rsquo;s Ubuntu minimal image.\nIt uses fbdev and allows to play confortably a lots of retro consoles including n64 and dreamcast.\nIt provides a way to scrap roms with skyscraper and convert de result into retroarch compatible playlists and thumbnails It has been designed for lowres display (cbvs or 480p).\u003c/p\u003e","title":"Odroid C1 fbdev Retroarch OS"},{"content":"It took me some months (!!) so I could invest a few hours to build my own arduboy. I had bought all needed parts a long time ago, but so many projects to do\u0026hellip; ;)\nNonetheless, it\u0026rsquo;s done and it was pretty simple, but yet fun to do. You will find below all the build steps\u0026hellip;\nObjective As a fan of both retrogaming and DIY electronic gadgets, I liked the Arduboy platform and decided to build a clone. I did not used a lipo battery to power it, but alkaline ones, just for the fun, even if it makes the console bigger at the end (but massive battery life of 30+ hours, and more \u0026ldquo;retro\u0026rdquo;)\nNeeded parts I used an Arduino pro micro as motherboard, which is not exactly \u0026ldquo;supported\u0026rdquo; by the Arduboy framework out of the box. Nevertheless, \u0026ldquo;MrBlinky\u0026rdquo; has made a custom framework which is fully compatible with Arduboy ecosystem and support alternative hardware, providing you wire everything correctly : https://github.com/MrBlinky/Arduboy-homemade-package/blob/master/README.md\nHere are all the parts I used: An Arduino Pro Micro 5v 16 Mhz One 4x6 centimeters Prototype Pcb One \u0026ldquo;5 Directions\u0026rdquo; tactile Switch, SMD 6 Pins 10x10x9mm Various U shape solderless breadboard jumper cable One mini slide switch One piezo ceramic wafer An 1.3\u0026quot; I2C IIC SPI Serial 128X64 White OLED LCD (7Pin, SH1106 controller) A 2-5v to 5v step-up converter Two micro tactile switch 6x6 mm Three leds, 3 mm (red, green and blue, as I did not have an rgb one\u0026hellip;) Three resistors A ribbon cable (an old IDE or floppy one) AAA battery springs and noses Some thermal pad Double sided adhesive Build Steps Put the screen on the center of the prototype PCB using double sided adhesive:\nSolder the 5 ways stick, the 2 tactile buttons, the leds and the resistors (on the back) and finally the slider switch :\nCut (shorten) the resistors cathode, start to make a common ground with resistors and buttons (use the cathode part you just cut to easily make a solder path):\nSolder the Arduino board on the back (behind the display), with the help of the resistors paws that have just been cut in the previous step and make a ground path for the 5 way switch which is on the other side :\nNow, wire and solder the 5 way tactile button pins to the corresponding Arduino pins (three wires are stacked, white, red and green on the photo and the fourth is soldered directly from the breadboard):\nWire the slide switch to the VCC input of the Arduino and add some ribbon cable wires to the resistors for the following step (notice the black lines I drawn to mark the future place of the step-up converted):\nPlace the step up converter, then wire the A and B buttons and one resistor to the Arduino, the slide switch to the the step up converter positive output pin and its ground pin to the a ground pin of the Arduino and to the ground path of the resistors we created in previous step (so the ground of leds and buttons is wired to the Arduino ground). Finally wire the 2nd resistor (the middle one) :\nIt\u0026rsquo;s time to wire the screen:\nAdd the piezo buzzer and put a game to check if its starts :\nAdd the battery springs and noses on the 3D printed back piece (see the link to the model at the end of the article) and solder wires:\nSolder battery holder wires to the step-up converted, then place the board on the case. Glue it or, as I did, block it with some thermal pad parts :\nGlue the back of the case (make sure the piezo plate is touching the back of the case to get some sound), put batteries, screw the battery cover, glue the button pads and\u0026hellip; switch on !\nUploading a Game Install the Arduino framework alternative on the Arduino IDE by Following the guide on github : https://github.com/MrBlinky/Arduboy-homemade-package/blob/master/README.md\nThen just get the source code of a game, remove batteries, plug the micro-usb cable in the Arduino.\nGo to the Tools menu and choose :\nBoard : \u0026ldquo;Home made arduboy\u0026rdquo; Based on : \u0026ldquo;Sparkfun Pro Micro 5v - alternate wiring\u0026rdquo; Code : \u0026ldquo;Arduboy optimized core Display : \u0026ldquo;SH1106\u0026rdquo; Bootloader : \u0026ldquo;original (caterina)\u0026rdquo; Flash select : \u0026ldquo;Pin0/D2/Rx (Recommended)\u0026rdquo; Upload the sketch\n3D model to print The case was made with freecad, sliced with Cura (4.1) and printed with an Anycubic I3 Mega.\nThe sources files are on Thingiverse (stl, and freecad files) https://www.thingiverse.com/thing:3774618\n","permalink":"https://www.bluemind.org/diy-tiny-arduboy-based-arduino-pro-micro/","summary":"\u003cp\u003eIt took me some months (!!) so I could invest a few hours to build my own arduboy. I had bought all needed parts a long time ago, but so many projects to do\u0026hellip; ;)\u003c/p\u003e\n\u003cp\u003eNonetheless, it\u0026rsquo;s done and it was pretty simple, but yet fun to do. You will find below all the build steps\u0026hellip;\u003c/p\u003e\n\u003ch2 id=\"objective\"\u003eObjective\u003c/h2\u003e\n\u003cp\u003eAs a fan of both retrogaming and DIY electronic gadgets, I liked the \u003ca href=\"https://arduboy.com\"\u003eArduboy platform\u003c/a\u003e and decided to build a clone. I did not used a lipo battery to power it, but alkaline ones, just for the fun, even if it makes the console bigger at the end (but massive battery life of 30+ hours, and more \u0026ldquo;retro\u0026rdquo;)\u003c/p\u003e","title":"DIY tiny Arduboy based on Arduino pro micro"},{"content":"Since several months now, I\u0026rsquo;m running my applications on a home made cluster of 4 Odroid HC1 running docker containers orchestrated by Swarm. Of course all HC1 are powered by my homemade powersupply.\nI chose HC1 and not MC1 because of SSD support. Running system on a SSD is a lot faster than on a microSD.\nBelow is the story of this build\u0026hellip;\n4 Odroid HC1 in a 3d printed 19\u0026quot; rack I made a custom fan cooled 19\u0026quot; rack mount support for the odroid.\nThe 3d print design is available on thingiverse\nInitial mount:\nI soldered dupond cables to power the fan directly from the 5V input of each HC1:\nResults with all fan mounted:\nFinal result in the rack:\nBase install Archlinux Well, nothing special here, just follow the official arch doc for each HC1: https://archlinuxarm.org/platforms/armv7/samsung/odroid-xu4\nSaltstack As I use saltstack to “templatize” all my servers, I installed saltstack master and minion (the NAS will be the master for all others servers). I already documented this here\nFrom the salt master, a simple check show that all node are under control:\nsalt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;cat /etc/hostname\u0026#39; node4.local.lan: node4 node3.local.lan: node3 node2.local.lan: node2 node1.local.lan: node1 SSD as root FS Partition ssd :\nsalt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;echo -e \u0026#34;o\\nn\\np\\n1\\n\\n\\n\\nw\\nq\\n\u0026#34; | fdisk /dev/sda\u0026#39; Format the futur root partition:\nsalt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;mkfs.ext4 -L ROOT /dev/sda1\u0026#39; Mount ssd root partition:\nsalt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;mount /dev/sda1 /mnt/\u0026#39; Clone sdcard to ssd root partition:\nsalt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;cd /;tar -c --one-file-system -f - . | (cd /mnt/; tar -xvf -)\u0026#39; Change boot parameters so root is /dev/sda1:\nsalt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;sed -i -e \u0026#34;s/root\\=PARTUUID=\\${uuid}/root=\\/dev\\/sda1/\u0026#34; /boot/boot.txt\u0026#39; Recompile boot config:\nsalt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;pacman -S --noconfirm uboot-tools\u0026#39; salt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;cd /boot; ./mkscr\u0026#39; Reboot:\nsalt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;reboot\u0026#39; Remove all from sdcard, and put /boot files at root of it:\nsalt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;mount /dev/mmcblk0p1 /mnt\u0026#39; salt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;cp -R /mnt/boot/* /boot/\u0026#39; salt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;rm -Rf /mnt/*\u0026#39; salt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;mv /boot/* /mnt/\u0026#39; Adapt boot.txt because boot files are in root of boot partition and no more in /boot directory:\nsalt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;sed -i -e \u0026#34;s/\\/boot\\//\\//\u0026#34; /mnt/boot.txt\u0026#39; salt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;pacman -S --noconfirm uboot-tools\u0026#39; salt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;cd /mnt/; ./mkscr\u0026#39; salt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;cd /; umount /mnt\u0026#39; salt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;reboot\u0026#39; Check that /dev/sda is root :\nsalt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#39;df -h | grep sda\u0026#39; node4.local.lan: /dev/sda1 118G 1.2G 117G 0% / node3.local.lan: /dev/sda1 118G 1.2G 117G 0% / node1.local.lan: /dev/sda1 118G 1.2G 117G 0% / node2.local.lan: /dev/sda1 118G 1.2G 117G 0% / SSD Benchmark Benchmarking is complex and I\u0026rsquo;m not going to say that I did perfectly right, but at least it gives an idea of how fast an SSD can be on HC1.\nThe SSD I connected to each of my HC1 is a Sandisk X400 128Gb.\nI Launched the following test 3 times.\nhdparm -tT /dev/sda =\u0026gt; 362.6 Mb/s\ndd write 4k =\u0026gt; 122 Mb/s\nsync; dd if=/dev/zero of=/benchfile bs=4k count=1048476; sync dd write 1m =\u0026gt; 119 Mb/s\nsync; dd if=/dev/zero of=/benchfile bs=1M count=4096; sync dd read 4k =\u0026gt; 307 Mb/s\necho 3 \u0026gt; /proc/sys/vm/drop_caches dd if=/benchfile of=/dev/null bs=4k count=1048476 dd read 1m =\u0026gt; 357 Mb/s\necho 3 \u0026gt; /proc/sys/vm/drop_caches dd if=/benchfile of=/dev/null bs=1M count=4096 I tried the same test with with IRQ affinity to big cores, but it did not shown any significant impact on performance.\nFinalize installation I’m not going to copy paste all my salstack states and templates here, as it obviously depends on personal needs and tastes.\nBasicaly, my “HC1 Node” template does the following on each node:\nChange mirrorlist Install custom sysadminscripts Remove alarmuser Add some sysadmin tools (lsof, wget, etc.) Change mmc and ssd scheduler to deadline Add my user Install cron Configure log rotate Set journald config (RuntimeMaxUse=50M and Storage=volatile to lesser flash storage writes) Add mail ability (ssmtp) Then changing password for my user using saltstack :\nsalt \u0026#34;node1.local.lan\u0026#34; shadow.gen_password \u0026#39;xxxxxx\u0026#39; # give password hash in return salt \u0026#34;node2.local.lan\u0026#34; shadow.set_password myuser \u0026#39;the_hash_here\u0026#39; Finaly, to ensure that not disk corruption would stop a node from booting, I forced fsck at boot time on all nodes by :\nadding \u0026ldquo;fsck.mode=force\u0026rdquo; in kernel line in /boot/boot.txt compile it with mkscr rebooting Docker Swarm deploy Swarm module in my saltstack seems not recognized despite I used the version 2018.3.1. So I ended up with executing commands directly, which is not really a problem as I\u0026rsquo;m not going to add a node everyday\u0026hellip;\nbuild the master:\nsalt \u0026#34;node1.local.lan\u0026#34; cmd.run \u0026#39;docker swarm init\u0026#39; add worker:\nsalt \u0026#34;node4.local.lan\u0026#34; cmd.run \u0026#39;docker swarm join --token xxxxx node1.local.lan:2377\u0026#39; add the 2nd and 3rd master for a failover ability:\nsalt \u0026#34;node1.local.lan\u0026#34; cmd.run \u0026#39;docker swarm join-token manager\u0026#39; salt \u0026#34;node3.local.lan\u0026#34; cmd.run \u0026#39;docker swarm join --token xxxxx 192.168.1.1:2377\u0026#39; salt \u0026#34;node2.local.lan\u0026#34; cmd.run \u0026#39;docker swarm join --token xxxxx 192.168.1.1:2377\u0026#39; Checking all nodes status with \u0026ldquo;docker node ls\u0026rdquo; now display one leader and 2 nodes \u0026ldquo;reachable\u0026rdquo;\nThen, I deployed a custom docker daemon configuration (daemon.json) to switch storage driver to overlay2 (the default one is to slow on xu4) and allows the usage of my custom docker registry.\n{ \u0026#34;insecure-registries\u0026#34;:[\u0026#34;myregistry.local.lan:5000\u0026#34;], \u0026#34;storage-driver\u0026#34;: \u0026#34;overlay2\u0026#34; } Docker images for the swarm cluster The concept As of now, using a container orchestrator implies to either use stateless containers or to use a global storage solution. I first tried to use glusterfs on all nodes. It was working perfectly but way to slow (between 25 and 36 Mb/s even with optimized settings and irq affinity to big cores).\nI ended up with a simple but yet very efficient solution for my needs :\nAn automated daily backup of all volumes on all nodes (to a network drive) An automated daily mysql database backup on all nodes (run only when mysql is detected) Containers that are able to restore their volumes from the backup during first startup An automated daily clean-up of containers and volumes on all nodes: Thus, each time a node is shut down or a stack restarted, each container is able to start on any nodes retrieving its data automatically (if not stateless).\nDaily backup script (extract):\n# monthly saved backup firstdayofthemonth=`date \u0026#39;+%d\u0026#39;` if [ $firstdayofthemonth == 01 ] ; then BACKUP_DIR=\u0026#34;$BACKUP_DIR/monthly\u0026#34; else firstdayoftheweek=$(date +\u0026#34;%u\u0026#34;) if [ day == 1 ]; then BACKUP_DIR=\u0026#34;$BACKUP_DIR/weekly\u0026#34; fi fi volumeList=$(ls /var/lib/docker/volumes | grep $DOCKER_VOLUME_LIST_PATTERN) for volume in $volumeList do archiveName=$(echo $volume | cut -d_ -f2-) mv \u0026#34;$BACKUP_DIR/$archiveName.tar.gz\u0026#34; \u0026#34;$BACKUP_DIR/$archiveName.tar.gz.old\u0026#34; cd /var/lib/docker/volumes/$volume/_data/ tar -czf $BACKUP_DIR/$archiveName.tar.gz * 2\u0026gt;\u0026amp;1 rm \u0026#34;$BACKUP_DIR/$archiveName.tar.gz.old\u0026#34; done Daily Clean-up script:\n# remove unused containers and images docker system prune -a -f # remove unused volumes volumeToRemove=$(docker volume ls -qf dangling=true) if [ ! -z \u0026#34;$volumeToRemove\u0026#34; ]; then docker volume rm $volumeToRemove fi Custom Docker images All my DockerFiles are documented and available on github :\nhttps://github.com/jit06/docker-images\nSimple distributed image build To make a simple distributed build system, I made some scripts to distribute my docker containers build across the 4 Odroid HC1.\nAll containers are then put in a local registry, tagged with the current date.\nLocal image builder that build, tag and put in registry (script name : docker_build_image) :\nif [ $# -lt 3 ]; then echo \u0026#34;Usage: $0 \u0026#34; echo \u0026#34;Example : $0 myImage armv7h myregistry.local.lan:5000\u0026#34; echo \u0026#34;\u0026#34; exit 0 fi arch=\u0026#34;$2\u0026#34; imageName=\u0026#34;$arch/$1\u0026#34; registry=\u0026#34;$3\u0026#34; tag=`date +%Y%m%d` docker build --rm -t $registry/$imageName:$tag -t $registry/$imageName:latest . docker push $registry/$imageName docker rmi -f $registry/$imageName:$tag docker rmi -f $registry/$imageName:latest Build several images given in argument (script name:docker_build_batch) :\n# usage : default build all if [[ \u0026#34;$1\u0026#34; == \u0026#34;-h\u0026#34; ]]; then echo \u0026#34;Usage: $0 [image folder 1] [image folder 2] ...\u0026#34; echo \u0026#34;Example :\u0026#34; echo \u0026#34; build two images : $0 mariadb mosquitto\u0026#34; echo \u0026#34;\u0026#34; exit 0 fi # if any parameter, use it/them as docker image to build if [[ $# -gt 0 ]]; then DOCKER_IMAGES_DIR=\u0026#34;${@:1}\u0026#34; else echo \u0026#34;Nothing to build. try -h for help\u0026#34; fi echo -e \u0026#34;\\e[1m--- going to build the following images :\u0026#34; echo -e \u0026#34;\\e[1m$DOCKER_IMAGES_DIR\\n\u0026#34; # build and send to repository for image in $DOCKER_IMAGES_DIR do echo -e \u0026#34;\\e[1m--- start build of $image:\u0026#34; cd /home/docker/$image docker_build_image $image armv7h myregistry.local.lan:5000 done Distribute builds using saltstack on the salt-master, using previous script.\nThe special image \u0026ldquo;archlinux\u0026rdquo; is built first if found, because all other images depend on it.\nDOCKER_IMAGES_DIR=\u0026#34;\u0026#34; SPECIAL_NAME=\u0026#34;archlinux_image_builder\u0026#34; NODES[0]=\u0026#34;\u0026#34; # usage : default build all if [[ \u0026#34;$1\u0026#34; == \u0026#34;-h\u0026#34; ]]; then echo \u0026#34;Usage: $0 [image folder 1] [image folder 2] ...\u0026#34; echo \u0026#34;Examples :\u0026#34; echo \u0026#34; build all found images : $0\u0026#34; echo \u0026#34; build two images : $0 mariadb archlinux_image_builder\u0026#34; echo \u0026#34;\u0026#34; exit 0 fi echo -e \u0026#34;\\e[1m--- Update repository (git pull)\\n\u0026#34; # update git repository cd /home/docker git pull # if any parameter, use it/them as docker image to build if [[ $# -gt 0 ]]; then DOCKER_IMAGES_DIR=\u0026#34;${@:1}\u0026#34; else DOCKER_IMAGES_DIR=$(ls -d */ | cut -f1 -d\u0026#39;/\u0026#39;) fi echo -e \u0026#34;\\e[1m--- going to build the following images :\u0026#34; echo -e \u0026#34;\\e[1m$DOCKER_IMAGES_DIR\\n\u0026#34; # if archlinux images in array, build it first if [[ $DOCKER_IMAGES_DIR = *\u0026#34;$SPECIAL_NAME\u0026#34;* ]]; then echo -e \u0026#34;\\e[1m--- found special image: $SPECIAL_NAME, start to build it first\u0026#34; echo -e \u0026#34;\\e[1m--- update repository on node1\\n\u0026#34; salt \u0026#34;hulk1.local.lan\u0026#34; cmd.run \u0026#34;cd /home/docker; git pull\u0026#34; echo -e \u0026#34;\\e[1m--- build $SPECIAL_NAME image on hulk1\\n\u0026#34; salt \u0026#34;hulk1.local.lan\u0026#34; cmd.run \u0026#34;cd /home/docker/$SPECIAL_NAME; ./mkimage-arch.sh armv7 registry.local.lan:5000\u0026#34; DOCKER_IMAGES_DIR=${DOCKER_IMAGES_DIR//$SPECIAL_NAME/} fi # update repository on all nodes echo -e \u0026#34;\\e[1m--- update repository on node[1-4]\\n\u0026#34; salt -E \u0026#34;node[1-4].local.lan\u0026#34; cmd.run \u0026#34;cd /home/docker; git pull\u0026#34; # Prepare build processes on known swarm nodes i=0 for image in $DOCKER_IMAGES_DIR do NODES[$i]=\u0026#34;${NODES[$i]} $image\u0026#34; i=$((i + 1)) if [[ $i -gt 3 ]]; then i=0 fi done echo -e \u0026#34;\\e[1m--- build plan :\u0026#34; echo -e \u0026#34;\\e[1m--- node1 : ${NODES[0]}\u0026#34; echo -e \u0026#34;\\e[1m--- node2 : ${NODES[1]}\u0026#34; echo -e \u0026#34;\\e[1m--- node3 : ${NODES[2]}\u0026#34; echo -e \u0026#34;\\e[1m--- node4 : ${NODES[3]}\\n\u0026#34; # distribute and launch build plan salt \u0026#34;node1.local.lan\u0026#34; cmd.run \u0026#34;docker_build_batch ${NODES[0]}\u0026#34; salt \u0026#34;node2.local.lan\u0026#34; cmd.run \u0026#34;docker_build_batch ${NODES[1]}\u0026#34; salt \u0026#34;node3.local.lan\u0026#34; cmd.run \u0026#34;docker_build_batch ${NODES[2]}\u0026#34; salt \u0026#34;node4.local.lan\u0026#34; cmd.run \u0026#34;docker_build_batch ${NODES[3]}\u0026#34; echo -e \u0026#34;\\e[1m--- build plan finished\u0026#34; ","permalink":"https://www.bluemind.org/odroid-hc1-based-swarm-cluster-19-rack/","summary":"\u003cp\u003eSince several months now, I\u0026rsquo;m running my applications on a home made cluster of 4 \u003ca href=\"https://www.hardkernel.com/shop/odroid-hc1-home-cloud-one/\"\u003eOdroid HC1\u003c/a\u003e running \u003ca href=\"https://www.docker.com/\"\u003edocker\u003c/a\u003e containers orchestrated by \u003ca href=\"https://docs.docker.com/engine/swarm/\"\u003eSwarm\u003c/a\u003e. Of course all HC1 are powered by my \u003ca href=\"/arduino-driven-5v-12v-smartpower/\"\u003ehomemade powersupply\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eI chose HC1 and not \u003ca href=\"https://www.hardkernel.com/shop/odroid-mc1-my-cluster-one-with-32-cpu-cores-and-8gb-dram/\"\u003eMC1\u003c/a\u003e because of SSD support. Running system on a SSD is a lot faster than on a microSD.\u003c/p\u003e\n\u003cp\u003eBelow is the story of this build\u0026hellip;\u003c/p\u003e\n\u003ch2 id=\"4-odroid-hc1-in-a-3d-printed-19-rack\"\u003e4 Odroid HC1 in a 3d printed 19\u0026quot; rack\u003c/h2\u003e\n\u003cp\u003eI made a custom fan cooled 19\u0026quot; rack mount support for the odroid.\u003c/p\u003e","title":"Odroid HC1 based swarm cluster in a 19\" rack"},{"content":"[caption id=\u0026ldquo;attachment_1755\u0026rdquo; align=\u0026ldquo;alignright\u0026rdquo; width=\u0026ldquo;300\u0026rdquo;] Odroid N1[/caption]\nThere is some months ago now, I was offered an ODROID N1 by Hardkernel for a debug party. Being short in time, I could not publish something earlier. But here it is, I used this SBC as a NAS to replace my Banana PI based solution.\nThe ODROID N1, is a lot more powerfull and I plan to use it for more than NAS activities in the future (I think about a custom docker registry and an ELK server).\nFor now, let\u0026rsquo;s focus on what I use it for now : a very good NAS !\nHardware 19\u0026quot; rack mount case I put all my servers (and my smartpower) in a 19\u0026quot; cabinet. This NAS makes no exception. To do so, I created a custom enclosure for the Odroid N1.\nFiles can be found on Thingiverse. I printed it in three parts so it fitted my 20x20x20 printer. However, the original Freecad source file is available.\nThe enclosure provides the following:\none 5.25\u0026quot; bay, for a removable 3.5\u0026quot; HDD for example one 3.5\u0026quot; bay for an full size HDD enough space behind the Odroid N1 to put a 2.5\u0026quot; device (e.g: a SSD) 2 holes for 40 mm fan some space for 2 leds (e.g: HDD activity leds) As seen in the photo below, I used the originals skrews and glued them on the printed enclosure to fix the Odroid N1:\nFinal result in my 19\u0026quot; cabinet (the white layer on the middle):\nUsed parts The whole solution is composed of:\n2 x 3.5\u0026quot; Sata HDD of 4TB mirrored on a nightly (I don\u0026rsquo;t like RAID with 2 disks) 1 x 120 GB SSD (kingston A400) used for /var (in prevision for ELK storage) 1 x 16 GB EMMC for root filesytem (given with the board) 2 leds and 2 resistors for disk activity monitoring A USB 3 gigabit ethernet adapter to add a second NIC Some wire for the 12V hack bellow 12v hack for molex connector The board being a \u0026ldquo;test model\u0026rdquo; it has some issues. Onw is missing 12V output on the onboard molex connector. As I had two 3.5\u0026quot; HDD to power, I did a little modification on the board, like someone else documented it on the official forum.\nThe hack simply consists in wiring the barrel connector input to the 12V pin of the molex connector. Of course, in that case the N1 has to be powered with 12v\u0026hellip;\nFront leds for mmc activity and heartbeat I wanted to have some emmc and heartbeat monitoring. I just made a small board with two leds and resistors. I wired them to the Odroid N1 GPIO and modified the kernel device tree mapping.\n[caption id=\u0026ldquo;attachment_1746\u0026rdquo; align=\u0026ldquo;alignnone\u0026rdquo; width=\u0026ldquo;609\u0026rdquo;] Leds board skrewed to the enclosure[/caption]\nIn order to make the leds working, I modified the device tree as following\nIn arch/arm64/boot/dts/rockchip/rk3399-odroidn1-linux.dts I added the following in the section \u0026ldquo;leds: gpio_leds\u0026rdquo; :\nfronthb { gpios = \u0026lt;\u0026amp;gpio1 13 GPIO_ACTIVE_HIGH\u0026gt;; linux,default-trigger = \u0026#34;heartbeat\u0026#34;; }; mmc { gpios = \u0026lt;\u0026amp;gpio1 18 GPIO_ACTIVE_HIGH\u0026gt;; linux,default-trigger = \u0026#34;mmc1\u0026#34;; }; Important : gpio number can be found in file include/dt-bindings/pinctrl/rk.h. In my case, GPIO 45 and 50 are named GPIO1_B.5 and GPIO1_C.2, so the corresponding numbers for the devicetree are GPIO1_B5 and GPIO1_C2 in the file rk.h.\nInitialization Install archlinux I received the Odroid N1 with a 16GB emmc module containing a Debian Linux. Being a fan of Archlinux I replaced the installed OS.\nThe is no official support form Archlinuxarm, but the Odroid N1 uses the same SOC than the supported ChromeBook (RK3399). So the main steps was to keep the kernel and modules and replace the root file system with archlinuxarm\u0026rsquo;s 64 bits root file-system archive.\nForm another computer, I mounted the emmc module:\ncp -R /lib/modules /some/safe/location rm -Rf / bsdtar archlinuxarm As there is no official support from archlinuxarm, the kernel should never be updated though pacman. So I added the following to /etc/pacman.conf:\nIgnorePkg = linux-aarch64* linux-firmware* updated all:\npacman -Syu Changed /etc/fstab to allow /boot (emmc) mount and specific options for / (emmc), /var (ssd) and /home (hdd)\n/dev/mmcblk1p1 /boot vfat defaults,noauto 0 0 /dev/mmcblk1p2 / ext4 defaults,noatime,nodiratime 0 1 UUID=xxxxxxxxx /var ext4 nodev,noatime,discard,errors=remount-ro 0 1 UUID=xxxxxxxxx /home ext4 nofail,noauto,nodev,nosuid,relatime,noexec,async,x-system.device-timeout010 0 2 To make it clear :\nDisable \u0026ldquo;atime\u0026rdquo; everywhere for performance and/or to lesser flash drives writes Activate \u0026ldquo;discard\u0026rdquo; option for the SSD to enable trim Enhance security a bit /home partition and make it never block the boot process (the system itself can leave without /home mounted) Finally, I moved back /home/alarm temporarily, just to be able to login as \u0026ldquo;alarm\u0026rdquo;, this user will be deleted later\u0026hellip;\nCross compiling the kernel After reboot was successful, it was time to build a new kernel.\nI used Linaro toolchain on m x86 laptop :\nwget https://releases.linaro.org/components/toolchain/binaries/latest-7/aarch64-linux-gnu/gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu.tar.xz tar -xf aarch64-linux-gnu/gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu.tar.xz Then I cloned the latest Odroid N1 kernel:\ngit clone https://github.com/hardkernel/linux.git --depth 1 -b odroidn1-4.4.y On my x86 Archlinux, I had to change the file in scripts/gcc-wrapper.py to use python2 (first line):\n#! /usr/bin/env python2 Then I compiled the kernel and installed modules to a dedicated directory to copy them on the Odroid N1 after the build:\nexport ARCH=arm64 export CROSS_COMPILE=/opt/gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu- make odroidn1_defconfig make menuconfig make -j5 mkdir /tmp/modules_N1 make INSTALL_MOD_PATH=/tmp/modules_n1 modules_install Regarding the initial kernel configuration, I modified the following options:\npreemption set to \u0026ldquo;server\u0026rdquo; : this is the true usage of my Odroid N1\u0026hellip; removed wireless : no need for me ipv4 connection traking support : needed for docker daemon all iptables option: needed for docker daemon bridge: needed for docker daemon Posix Mqueue: needed for docker registry After the build finished, I copied DTB file and Image to /boot (on Odroid N1) and /tmp/modules/4.4.114 to /lib/modules (on Odroid N1 too)\nThen on Odroid N1 :\n$ reboot [...] $ uname -a Linux filesrv 4.4.114 #2 SMP Sat Jun 30 16:59:47 CEST 2018 aarch64 GNU/Linux Deploy saltstack As I use saltstack to \u0026ldquo;templatize\u0026rdquo; all my servers, I installed saltstack master and minion (the NAS will be the master for all others servers). I already documented this here\nI\u0026rsquo;m not going to copy paste all my states and templates here, as it obviously depends on personnal needs and tastes.\nBasicaly, my \u0026ldquo;NAS\u0026rdquo; template did the following on the Odroid N1:\nConfigures my HDDs (mirroring, etc.) Set CPU affinity (see benchmarks below) Optimized network parameters for gigabit Created / removed users (e.g: remove alarm user) Install NFS and Samba Install docker and deploy a local registry Some benchmarks The purpose of theses benchmarks was to see what the Odroid N1 had to offer. I also used them to tune up and adapt my configuration.\nBenchmarking is a quite complex task and I don\u0026rsquo;t claim that I did it 100% correctly !\nHDD / SATA Test protocol For every parameters I tested, I launched 3 times the following tests\nhdparm -tT /dev/sda\ndd write 4k:\nsync; dd if=/dev/zero of=/home/datas/benchfile bs=4k count=1048476; sync dd write 1m:\nsync; dd if=/dev/zero of=/home/datas/benchfile bs=1M count=4096; sync dd read 4k:\necho 3 \u0026gt; /proc/sys/vm/drop_caches dd if=/home/datas/benchfile of=/dev/null bs=4k count=1048476 dd read 1m:\necho 3 \u0026gt; /proc/sys/vm/drop_caches dd if=/home/datas/benchfile of=/dev/null bs=1M count=4096 Tuned parameters Set readahead\nhdparm -a 1024 /dev/sda Set performance for pcie_aspm (for cold boot, add \u0026ldquo;pcie_aspm.policy=performance\u0026rdquo; to boot parameters):\necho performance \u0026gt; /sys/module/pcie_aspm/parameters/policy set cpu performance\necho performance \u0026gt; /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor echo performance \u0026gt; /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor echo performance \u0026gt; /sys/devices/system/cpu/cpu2/cpufreq/scaling_governor echo performance \u0026gt; /sys/devices/system/cpu/cpu3/cpufreq/scaling_governor echo performance \u0026gt; /sys/devices/system/cpu/cpu4/cpufreq/scaling_governor echo performance \u0026gt; /sys/devices/system/cpu/cpu5/cpufreq/scaling_governor Conclusion Setting readahead to 1024 provided a small enhancement (read). Setting \u0026ldquo;pcie_aspm\u0026rdquo; to \u0026ldquo;performance\u0026rdquo; gave also another small enhancement while writing.\nCPU governor did not to affect that much hdd performance (default is interactive), but I probably reached my HDD max speed\u0026hellip;\nSSD on USB3 VS Sata I initially planned to connect the SSD thought USB3. The following tests changed my mind, and I finally connected the SSD throught the native SATA port and the mirrored HDD to USB3.\nI used the same test protocols than previously with the HDD.\nHere are the results:\nNative Network I only did very basic tests with FTP, Samba and NFS. The change of \u0026ldquo;txqueuelen\u0026rdquo; was the only parameter that had a significant impact on write (18%)\nI measured the following performance on my network with a 8 Gb file:\nFTP : read = 112 Mb/s, write = 90 Mb/s NFS : read = 106 Mb/s, write = 54 Mb/s SMB: read = 100 Mb/s, write = 53 Mb/s Below are the three kind of tuning I tried:\nChanging \u0026ldquo;txqueuelen\u0026rdquo; (18% faster on write):\ntxqueuelen 10000: /sbin/ip link set eth0 txqueuelen 10000 Assigning eth0 to big cores (no impact on performance)\necho 4-5 \u0026gt; /proc/irq/24/smp_affinity_list changing tcp stack settings (no impact on performance)\nsysctl -w net.core.rmem_max=8738000 sysctl -w net.core.wmem_max=6553600 sysctl -w net.ipv4.tcp_rmem=\u0026#34;8192 873800 8738000\u0026#34; sysctl -w net.ipv4.tcp_wmem=\u0026#34;4096 655360 6553600\u0026#34; sysctl -w net.ipv4.tcp_timestamps=0 sysctl -w net.ipv4.tcp_window_scaling=1 sysctl -w net.ipv4.tcp_sack=1 sysctl -w net.ipv4.tcp_no_metrics_save=1 sysctl -w net.ipv4.conf.all.arp_ignore=1 sysctl -w net.ipv4.conf.all.arp_filter=1 ","permalink":"https://www.bluemind.org/odroid-n1-enhanced-nas-19-rack/","summary":"\u003cp\u003e[caption id=\u0026ldquo;attachment_1755\u0026rdquo; align=\u0026ldquo;alignright\u0026rdquo; width=\u0026ldquo;300\u0026rdquo;] \u003ca href=\"images/Odroid-N1-Mini-PC-684x500.jpg\"\u003e\u003cimg\n  src=\"images/Odroid-N1-Mini-PC-684x500.jpg\"\n  alt=\"ODROID-N1 single-board computer with cooling fan\"\n  class=\"article-image\"\n  loading=\"lazy\"\n\u003e\u003c/a\u003e Odroid N1[/caption]\u003c/p\u003e\n\u003cp\u003eThere is some months ago now, I was offered an ODROID N1 by Hardkernel for a debug party. Being short in time, I could not publish something earlier. But here it is, I used this SBC as a NAS to replace my Banana PI based solution.\u003c/p\u003e\n\u003cp\u003eThe ODROID N1, is a lot more powerfull and I plan to use it for more than NAS activities in the future (I think about a custom docker registry and an ELK server).\u003c/p\u003e","title":"Odroid N1 as NAS in a 19\" rack"},{"content":"I was updating my personal home \u0026ldquo;pico data center\u0026rdquo; in order to get more processing power (swarm cluster) and arrange all my ARM based servers in a cabinet.\nI wanted to use only on power supply for all my servers (mostly arm based) instead of multiple 5v power adapters. I also had some 12v needs for my router and my NAS running an Odroid N1 (more to come about it in a future article\u0026hellip;)\nSo I built a custom 1U rackable smart power which provides some remote services through HTTP\u0026hellip;\nFeatures The smart power actually has the following features :\n5V 20A capacity 12V 8.5A capacity 6 USB connectors for 5V supply 2 Barrel connectors for 12V supply One temperature sensor Live display of power consumption and temperature Hardware switch to turn on or off any of the 8 power connectors \u0026ldquo;Cold\u0026rdquo; power-on sequence for all connectors (eg. turn on servers in the right order) Gather temperature and consumption history through REST API (json) Toggle any connector\u0026rsquo;s status thought HTTP command Used Hardware Support pieces A 19 inches 1U metal shelf for the base A small piece of wood (1cm thick) to fix all electronic parts Some wood skrews A piece of plastic I had to cover electronic parts (seems to be a fridge\u0026rsquo;s bottom grid\u0026hellip;) A \u0026ldquo;U\u0026rdquo; in metal to fix the female main power plug Electronics parts Two power supplies : one of 5V 20A and one of 12v 8.5A An Arduino Mega (more memory for data history) A W5100 ethernet shield A 8 relays board that provides a dedicated power line to drive relays USB and Barrel connectors A TM1638 Board (display + buttons) Cables (dupont, and others small ones) 2 HAL effect sensors rated for 20A A cheap DTH11 temp sensor Some prototyping boards A 40mm fan Assembly Soldering connectors and placing main parts on the shelf:\nPreparing DTH11 and start wiring everthing:\nPutting the fan and covering with the plastic grid:\nArduino sketch Wiring plan The following wiring plan is fully compatible with Nano and Uno. However, I used an Arduino Mega in order to have a bigger data history (more ram).\nCode organization The source code of the Arduino sketch if available on my github : https://github.com/jit06/smartpower\nThe code should be commented enough to make it easy to read and understand.\nSmartPower.ino : the main arduino sketch, kept small to drive the main logic Settings.h: contains all things that can be customized without touching a line of code Controller.cpp and Controller.h: code that allows to drive the TM1638 module Http.cp and Http.h: handle REST API to get json data and change relays\u0026rsquo; states through HTTP queries Sensors.cpp and Sensors.h: handle hall effect and temperature sensors as well as in-memory data storage Timehandler.cpp and Timehandler.h: allow to set time via NTP in order to add a timestamp for each recorded value Local Usage Schema of how the smartpower hardware interface works:\nBelow a photo of the smartpower in action on my 19 inches cabinet:\nRemote Usage (HTTP) \u0026ldquo;/\u0026rdquo; (default page) : gives a html page with live 5v and 12v power consumption (in Ah), total power consumption (in Watts) and current temperature \u0026ldquo;/TEM\u0026rdquo;: gives current temperature in JSON \u0026ldquo;/05V\u0026rdquo;: gives 5V live power consumption (Ah) in JSON \u0026ldquo;/12V\u0026rdquo;: gives 12V live power consumption (Ah) in JSON \u0026ldquo;/PWR\u0026rdquo;: gives live total power consumption (Watts) in JSON \u0026ldquo;/ALL\u0026rdquo;: gives all previous values in one JSON \u0026ldquo;/HIS\u0026rdquo;: gives history of all values with timestamps in one JSON \u0026ldquo;/TGx\u0026rdquo;: where x between 0 and 7, toggles switch status if option is enabled (make sure to use it in a secure LAN, has there is no HTTPS support) Sample of HTML page Note that the default HTML page is refreshed automatically every 10 seconds (http header \u0026ldquo;Refresh: 10\u0026rdquo;).\nSample of \u0026ldquo;/ALL\u0026rdquo; response { \u0026#34;temp\u0026#34;:\u0026#34;30.53\u0026#34;, \u0026#34;5V current consumption in Ah\u0026#34;:\u0026#34;2.86\u0026#34;, \u0026#34;12V current consumption in Ah\u0026#34;:\u0026#34;1.60\u0026#34;, \u0026#34;Power consumption in Watt\u0026#34;:\u0026#34;30.31\u0026#34; } Sample of \u0026ldquo;/HIS\u0026rdquo; response { \u0026#34;values\u0026#34;:[ { \u0026#34;temp_ts\u0026#34;:\u0026#34;1535055136\u0026#34;, \u0026#34;temp\u0026#34;:\u0026#34;28.00\u0026#34; }, { \u0026#34;current5v_ts\u0026#34;:\u0026#34;1535055136\u0026#34;, \u0026#34;current5v\u0026#34;:\u0026#34;2.77\u0026#34; }, { \u0026#34;current12v_ts\u0026#34;:\u0026#34;1535055136\u0026#34;, \u0026#34;current12v\u0026#34;:\u0026#34;1.32\u0026#34; }, { \u0026#34;power_ts\u0026#34;:\u0026#34;1535055137\u0026#34;, \u0026#34;power\u0026#34;:\u0026#34;29.84\u0026#34; }, [...] {\u0026#34;end\u0026#34;:\u0026#34;OK\u0026#34;} ] } ","permalink":"https://www.bluemind.org/arduino-driven-5v-12v-smartpower/","summary":"\u003cp\u003eI was updating my personal home \u0026ldquo;pico data center\u0026rdquo; in order to get more processing power (swarm cluster) and arrange all my ARM based servers in a cabinet.\u003c/p\u003e\n\u003cp\u003eI wanted to use only on power supply for all my servers (mostly arm based) instead of multiple 5v power adapters. I also had some 12v needs for my router and my NAS running an Odroid N1 (more to come about it in a future article\u0026hellip;)\u003c/p\u003e","title":"Arduino driven 5v and 12v Smartpower"},{"content":"My son bought a X-Craft Hovercraft (http://www.rcmania.com/fastlane-rc-x-craft-hovercraft). It was cheap (end of life) and announced with a \u0026ldquo;12v power\u0026rdquo; where a lot of similar products were 9.6v.\nIndeed, 12v is really a plus and the hovercraft is pretty fast. Unfortunately, the battery is a good old NiMH rated at\u0026hellip; 600 mAh (10 x 1.2v cells, near half aaa in size) ! It last between 8 and 10 minutes for a charging time of around 5 hours. It was difficult to find another one at a reasonable price and the charging time had to be monitored carefully (no auto off).\nSo, I decided to build two 12v LiPo (11.1v to be exact) with three 18650 cells. I had to change the way the battery could be plugged on the Hovercraft.\nI also used a charger I already had for heated gloves, which was able to charge two 12v LiPo at the same time.\nThe final result : 30 minutes of play time with one battery, 4h to recharge both with an auto stop ability (mandatory for LiPo batteries).\nUsed materials for each battery: 3 x NCR18650B batteries 1 x 3S LiPo PCB protection board with balance function 1 x 12v round connectors 3 x solder tabs 1 x DC connector (5.5x2.1 mm) Some adhesive tape Soldering cells in series and wire the 3S protective board:\nAttaching the DC male connector and finish with the adhesive tape:\nSoldering the female connector to the Hovercraft and plug the battery\n","permalink":"https://www.bluemind.org/xcraft-hovercraft-battery-replacement-lipo/","summary":"\u003cp\u003eMy son bought a X-Craft Hovercraft (\u003ca href=\"http://www.rcmania.com/fastlane-rc-x-craft-hovercraft\"\u003ehttp://www.rcmania.com/fastlane-rc-x-craft-hovercraft\u003c/a\u003e). It was cheap (end of life) and announced with a \u0026ldquo;12v power\u0026rdquo; where a lot of similar products were 9.6v.\u003c/p\u003e\n\u003cp\u003eIndeed, 12v is really a plus and the hovercraft is pretty fast. Unfortunately, the battery is a good old NiMH rated at\u0026hellip; 600 mAh (10 x 1.2v cells, near half aaa in size) ! It last between 8 and 10 minutes for a charging time of around 5 hours. It was difficult to find another one at a reasonable price and the charging time had to be monitored carefully (no auto off).\u003c/p\u003e","title":"X-Craft Hovercraft battery replacement with LiPo"},{"content":"Yes, one more retrogaming device ! This time, I wanted to try a smaller form factor, as near as possible to the GameBoy Micro. I also modelized my own enclosure and 3d printed it. The design is near the GamOdroiD C0 but with slightly curved edges which, from my point of view, looks better.\nFeatures Hardware Specifications Small form factor : 102x51x23 mm Tiny 2 inches display Numeric sound (I2S) 8 buttons : a,b,x,y, L, R, start, select 2500 mAh for near 4h of runtime HDMI out 1x USB 2 port (micro usb) Charge with standard micro usb power supply leds to display states : charging, charged, out of battery (white led in front) Supported gaming system I only installed systems I was interested for, but it can of course run all systems the Pi Zero supports with retropie :\nAtari 7600 Game Gear Atari Lynx Game Boy \u0026amp; Game Boy Color GameBoy advance Nes Famicom disk system Master System Pc Engine, including CD-Rom Megadrive / Genesis Sega-CD Snes CPS 1, 2 and 3 (FBA) Various ports : Doom 1 \u0026amp; 3, Duke 3d, Outrun, Wolfenkein 3d, rick dangerous, Open Tyrian Used Components Main parts RaspberryPi Zero PowerBoost 1000C MAX98357 I2S Class-D Mono Amp Noname Lipo battery from ebay (91x42x5.1mm) 64Gb Samsung EVO Micro SD 2″ NTSC/PAL TFT Display Small copper squares for cooling Thermal paste Control parts 12 Soft Tactile Buttons (8mm) 2 silent micros switch buttons (6x5mm) Various other electronic parts Old IDE Ribbon cable for wires 3mm white led 470K resistor Mini switch (8.5x3.7) Breadboard Decoration parts Some Nail polish templates for colors (black, yellow, red, green, blue). Found on eBay. XTC 3D White satin Spray paint 3D model and printing for the case I created the case from scratch with Freecad. The inspiration comes from the GameBoy micro and classical snes colors for the buttons. However, the final result is a little more thick than the original GameBoy micro (17.2 mm VS 23 mm) due to physical constraints (height of Pi Zero connectors as well as the battery size).\nFreecad source files are available on github. Ready to slice and print STL files are published on Thingiverse.\nOverview of all printed pieces before painting:\nFront part, inside:\nBack part, inside. The photo does not show the final version I used. The latest version is slightly taller, the battery blockers are balanced and screw supports contain pre-holes for screws (see the 3d model).\nFinished front side (I used similar tips than for the GamOdroid C0):\nHardware assembly Hardware assembly was not difficult : no hack, just plain simple usage of standard boards, and some cut breadboards for buttons.\nPreparing boards First step : soldering wires on the pi zero, the i2s audio amp and the powerboost 1000c.\nNote that I removed the jst connector from the powerboost to limit the final result\u0026rsquo;s thickness.\nBuilding buttons boards I used some breadboards and a dremel to make shapes adjusted to the body of the console.\nMounting and screwing all pieces Everything was ready to be screwed on both back and front parts of the enclosure.\nNote that the height of screw supports just fit the pi zero\u0026rsquo;s connectors height and allows to hold in place 3 pieces of copper to CPU cooling (with thermal paste).\nThe speaker has been glued with some cyanoacrylate glue.\nThe black and red wires are for the battery connection.\nLinking all together Final step : add the display (glued with hot glue), solder wires for buttons and speaker, and add the battery.\nResult of the assembly The final result : it can be stored in a small child\u0026rsquo;s glasses box ;)\nThe last photo show the console next to it\u0026rsquo;s great sister, the GamOdroid C0.\nSoftware installation This part was probably the easiest one, thanks to the raspberry pi community. All is working out of the box with retropie. I just made some adjustments to make it boots faster and to tune the display.\nInitial setup This step brought a fully functional system: Deploy retropie on the microSd : https://retropie.org.uk/download/ Launch a full upgrade through retropie setup menu Following the I2S amp guide: https://learn.adafruit.com/adafruit-max98357-i2s-class-d-mono-amp/raspberry-pi-usage Install emulators Copy Bios and Roms Launch the video scraper : https://github.com/retropie/retropie-setup/wiki/scraper Get retrogame and adjust config file regarding GPIO wiring : https://github.com/adafruit/Adafruit-Retrogame Retropie and Emulationstation Optimizations From retropie-setup menu I did the following:\nremove samba and configuration remove usbromservice remove splash screen And in emulationstation:\nin Sound settings, set OMX Player Audio device to \u0026ldquo;Alsa:HW:0,0\u0026rdquo; In Other settings, set \u0026ldquo;parse gameslists only\u0026rdquo; and \u0026ldquo;Use OMX Player\u0026rdquo; to \u0026ldquo;ON\u0026rdquo; System Optimizations I modified the /boot/config.txt file to overclock the pi zero, limit GPU mem to 128 Mb and disable splash screen (faster boot):\ngpu_mem=128 disable_splash=1 gpu_freq=500 core_freq=500 sdram_freq=500 dtparam=sd_overclock=100 I also modified the /boot/cmdline.txt file, mainly to speedup the boot process :\nremoved \u0026ldquo;console=serial0,115200\u0026rdquo; set elevator to \u0026ldquo;noop\u0026rdquo; added \u0026ldquo;udev.log-priority=3 quiet fastboot noswap lpj=3489792 rd.systemd.show_status=false\u0026rdquo; I tuned fstab to make root fs a little bit faster and set /var/log as a tmpfs filesystem to preserve the microSD card\u0026rsquo;s life (it maybe also make things a little bit faster because logs are written to ram):\n/dev/mmcblk0p1 /boot vfat defaults 0 0 /dev/mmcblk0p1 /boot ext4 defaults,noatime,data=ordered 0 0 tmpfs /var/log tmpfs nodev,nosuid,noatime,size=20M 0 0 In /etc/rc.local\nI removed all the code related to showing the IP address of the Pi I added \u0026ldquo;retrogame \u0026amp;\u0026rdquo; to launch the gpio buttons driver In /etc/Systemctl/journalctl.conf\nI set \u0026ldquo;Storage=none\u0026rdquo; to disable any logging activity Finaly, I disabled all the following services (sudo systemctl disable ):\nntp fake-hwclock hwclock-save rsyslog syslog swap avahi-daemon bluetooth cron dbus-org.bluez dbus-org.freedesktop.Avahi hciuart rpi-display-backlight networking dhcpcd dphys-swapfile ssh systemd-journal-flush plymouth plymouth-log All these operations gave an acceptable boot time for a portable gaming console :\n$ systemd-analyze Startup finished in 1.356s (kernel) + 7.972s (userspace) = 9.328s Final Result: Photos \u0026amp; videos ","permalink":"https://www.bluemind.org/piboy-micro-raspberry-pi-based-mini-portable-retrogaming/","summary":"\u003cp\u003eYes, one more retrogaming device ! This time, I wanted to try a smaller form factor, as near as possible to the \u003ca href=\"https://en.wikipedia.org/wiki/Game_Boy_Micro\"\u003eGameBoy Micro\u003c/a\u003e.\nI also modelized my own enclosure and 3d printed it. The design is near the \u003ca href=\"/gamodroid-c0-odroid-based-portable-retrogaming/\"\u003eGamOdroiD C0\u003c/a\u003e but with slightly curved edges which, from my point of view, looks better.\u003c/p\u003e\n\u003ch1 id=\"features\"\u003eFeatures\u003c/h1\u003e\n\u003ch2 id=\"hardware-specifications\"\u003eHardware Specifications\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eSmall form factor : 102x51x23 mm\u003c/li\u003e\n\u003cli\u003eTiny 2 inches display\u003c/li\u003e\n\u003cli\u003eNumeric sound (I2S)\u003c/li\u003e\n\u003cli\u003e8 buttons : a,b,x,y, L, R, start, select\u003c/li\u003e\n\u003cli\u003e2500 mAh for near 4h of runtime\u003c/li\u003e\n\u003cli\u003eHDMI out\u003c/li\u003e\n\u003cli\u003e1x USB 2 port (micro usb)\u003c/li\u003e\n\u003cli\u003eCharge with standard micro usb power supply\u003c/li\u003e\n\u003cli\u003eleds to display states : charging, charged, out of battery (white led in front)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"supported-gaming-system\"\u003eSupported gaming system\u003c/h2\u003e\n\u003cp\u003eI only installed systems I was interested for, but it can of course run all systems the Pi Zero supports with retropie :\u003c/p\u003e","title":"PiBoy Micro : a Raspberry pi zero based mini portable retrogaming"},{"content":"Since a couple of years, I used to use a french home automation box : the Zibase. In its time, it was the only one to support a large number of protocols and to allow a some degrees of freedom (xml output, UDP access). I even wrote a binding for Openhab. But even with OpenHab, the box still needs a dedicated cloud service to register some peripherals, and nowdays, the number of supported protocols seems pretty limited compared to Rflink.\nSo, I decided to build my own Rflink box, including an homemade \u0026ldquo;all-in-one\u0026rdquo; arduino shield that provides Rf modules and serial to MQTT/Json converter for Rflink messages.\nSource code is available on github : https://github.com/jit06/RflinkToJsonMqtt 3d enclosure is available on thingiverse : https://www.thingiverse.com/thing:2600538\nRflink runs on an arduino mega (2560) and need some easy to find RF modules. You can even found shields that work out-of-the-box.\nThe problem is that Rflink output is plain text over serial. As I wanted something more easy to broadcast and decoupled from my automation server (OpenHab), I chose tcp/ip for the transport layer, and json for the message format (simple to parse and human readable). Thus, MQTT seemed to be the easiest protocol to use.\nFor now, my Rflink box only support 433 Mhz protocols, but I\u0026rsquo;m planning to add 868 Mhz.\nHardware build Used parts Arduino Mega 2560 (Rflink) RBX6 as 433 Mhz receiver The cheap and well known XD-FST FS1000A for transmitting An external high gain Antena for very good reception (the most critical thing) An internal spring antenna to improve orders sending One SMA connector An Arduino nano for the Serial to MQTT/Json convertion A mini W5100 ethernet shield for TCP/IP transport 2 prototypes PCBs Male Pin headers Build steps Prepare the shield with prototype PCBs and pin header:\nThen start adding and soldering components on the shield:\nFinaly, wire and solder everything:\nThe shield can then be mounted on top the the Mega (the nano is powered by the Mega from the 5V-out pin to the nano\u0026rsquo;s V-in. Cable is only used for debugging / programming purpose on the nano)\nNote that detailed instructions can be found on Rflink website for Rf modules and everywhere on the internet for Arduino nano and w5100 module.\n3D printed enclosure Because the \u0026ldquo;wife compliant\u0026rdquo; property does always mater for homemade devices, a nice enclosure is a must\u0026hellip; Here are photos of the box that host my Rflink+Shield package.\nSerial to MQTT/Json converter The arduino sketch is available on github : https://github.com/jit06/RflinkToJsonMqtt\nIt runs on an arduino nano / uno. Mine is running since more than 30 days now, 3 to 5 messages per seconds without any problem.\nThe goal was to make the sketch as fast as possible regarding the serial to json translation. This translation can be seen in Rflink.cpp, function readRfLinkFields(). Basicaly, it does a string copy with some modifications while copying in order to create a valid json string on the fly:\nvoid readRfLinkFields(char* fields, int start){ int strpos=start; int fldpos=0; int valueType=0; JSON[0]=\u0026#39;{\u0026#39;; JSON[1]=\u0026#39;\\0\u0026#39;; while(strpos \u0026lt; BUFFER_SIZE-start \u0026amp;\u0026amp; fields[strpos] != \u0026#39;\\n\u0026#39; \u0026amp;\u0026amp; fields[strpos] != \u0026#39;\\0\u0026#39;) { // if current char is \u0026#34;=\u0026#34;, we end name parsing and start parsing the field\u0026#39;s value if(fields[strpos] == \u0026#39;=\u0026#39;) { FIELD_BUF[fldpos]=\u0026#39;\\0\u0026#39;; fldpos=0; // Tag field regarding the name... if(RfLinkFieldIsString(FIELD_BUF)) valueType=RFLINK_VALUE_TYPE_STRING; else if(RfLinkFieldIsOregon(FIELD_BUF)) valueType=RFLINK_VALUE_TYPE_OREGON; else if(RfLinkFieldIsHexInteger(FIELD_BUF)) valueType=RFLINK_VALUE_TYPE_INTEGER; else valueType=RFLINK_VALUE_TYPE_RAWVAL; RfLinkFieldAddQuotedValue(FIELD_BUF); // if current char is \u0026#34;;\u0026#34;, we end parsing value and start parsing another field\u0026#39;s name } else if(fields[strpos] == \u0026#39;;\u0026#39;) { FIELD_BUF[fldpos]=\u0026#39;\\0\u0026#39;; fldpos=0; strcat(JSON,\u0026#34;:\u0026#34;); // Handle special cases... switch(valueType) { case RFLINK_VALUE_TYPE_STRING: RfLinkFieldAddQuotedValue(FIELD_BUF); break; case RFLINK_VALUE_TYPE_OREGON: RfLinkFieldAddOregonValue(FIELD_BUF); break; case RFLINK_VALUE_TYPE_INTEGER: RfLinkFieldAddIntegerValue(FIELD_BUF);break; default : strcat(JSON,FIELD_BUF); } strcat(JSON,\u0026#34;,\u0026#34;); } else { // default case : copy current char FIELD_BUF[fldpos++]=fields[strpos]; } strpos++; } int len = strlen(JSON); JSON[len-1]=\u0026#39;}\u0026#39;; } The whole code is optimized to use the program memory as much as possible.\nExample of output with mosquitto:\n#\u0026gt; mosquitto_sub -h mqtt.local.lan -v -t \u0026#39;#\u0026#39; rflink/OregonV1/000A {\u0026#34;TEMP\u0026#34;:19.6,\u0026#34;BAT\u0026#34;:\u0026#34;OK\u0026#34;} rflink/X2D/003f040 {\u0026#34;SWITCH\u0026#34;:\u0026#34;21\u0026#34;,\u0026#34;CMD\u0026#34;:\u0026#34;ON\u0026#34;,\u0026#34;EXT\u0026#34;:PIR,\u0026#34;BAT\u0026#34;:\u0026#34;OK\u0026#34;} rflink/Oregon_TempHygro/2D27 {\u0026#34;TEMP\u0026#34;:20.6,\u0026#34;HUM\u0026#34;:41,\u0026#34;HSTATUS\u0026#34;:1,\u0026#34;BAT\u0026#34;:\u0026#34;OK\u0026#34;} Note that for incoming messages, this sketch acts as a pure Ethernet gateway. That is to say, any message published on the configured channel will be caught and sent as a plain serial message to Rflink.\nUsage example with OpenHab First, Openhab must be able to subscribe to your mqtt server (mosquitto in my case). I used the addon \u0026ldquo;org.openhab.binding.mqtt\u0026rdquo; and named the mqtt instance \u0026ldquo;mosquitto\u0026rdquo;\nThe previously seen Oregon temperature and humidity sensor (ID \u0026ldquo;2D27\u0026rdquo;) can then be used with some items :\nNumber Temp \u0026#34;Temperature [%.1f ]\u0026#34; { mqtt=\u0026#34;\u0026lt;[mosquitto:rflink/Oregon_TempHygro/2D27:state:JSONPATH($.TEMP)]\u0026#34; } Number Hum \u0026#34;Humidity [%d %%]\u0026#34; { mqtt=\u0026#34;\u0026lt;[mosquitto:rflink/Oregon_TempHygro/2D27:state:JSONPATH($.HUM)]\u0026#34; } String Bat \u0026#34;Battery state [%s]\u0026#34; { mqtt=\u0026#34;\u0026lt;[mosquitto:rflink/Oregon_TempHygro/2D27:state:JSONPATH($.BAT)]\u0026#34; } Another example, this time to send an order to a Chacon DIO switch with RFlink:\nSwitch mySwitch \u0026#34;a switch test\u0026#34; { mqtt=\u0026#34;\u0026gt;[mosquitto:rflink/Order:command:ON:10;NewKaku;000500;1;ON;], \u0026gt;[mosquitto:rflink/Order:command:OFF:10;NewKaku;0000500;1;OFF;]\u0026#34; } As you can see the mqtt message is a plain text message as you would send it to Rflink through a serial connection :\n10;NewKaku;0000500;1;OFF; ","permalink":"https://www.bluemind.org/custom-arduino-shield-mqtt-rflink/","summary":"\u003cp\u003eSince a couple of years, I used to use a french home automation box : the \u003ca href=\"https://www.zodianet.com/toolbox-zibase/zibase-classic.html\"\u003eZibase\u003c/a\u003e. In its time, it was the only one to support a large number of protocols and to allow a some degrees of freedom (xml output, UDP access). I even wrote a \u003ca href=\"/project-update-openhab-binding-zibase/\"\u003ebinding for Openhab\u003c/a\u003e. But even with OpenHab, the box still needs a dedicated cloud service to register some peripherals, and nowdays, the number of supported protocols seems pretty limited compared to \u003ca href=\"http://www.rflink.nl/blog2/\"\u003eRflink\u003c/a\u003e.\u003c/p\u003e","title":"Custom Arduino Shield that provides MQTT to Rflink"},{"content":"Since a few years now, I used to use a Sheevaplug as a NAS (see /linux-sheevaplug-perfect-nas and /linux-sheevaplug-perfect-nas-reloaded). The Sheevaplug was running fine, but I needed some more processing power and memory to make my document search as well as SMB file copy faster and host a saltstack master.\nThis install has been made with custom saltstack states from another machine before transforming itselft in my saltstack master.\nBase install Archlinux Installing Archlinux on a Bananapi was not officialy supported (at time where I did the install, 1 year ago), but as the hardware is very similar to cubieboard 2, it just needed a dedicated uBoot. Hopefully, someone did it :\nhttp://archlinuxarm.org/forum/viewtopic.php?f=27\u0026amp;t=9445\nI also optimized the SDCard by formating it with (see here for more info):\nmkfs.ext4 -O ^has_journal -E stride=2,stripe-width=1024 -L \u0026#34;bananapi\u0026#34; -b 4096 -v -n /dev/sdb1 Then I changed root password (su, passwd) and changed the hostname (/etc/hostname)\nFinaly I copied my custom Salt-stack minion installer script (see here) right after a full system upgrade:\npacman -Syu chmod +x salt-minion-install.sh ./salt-minion-install.sh On the salt master side, thanks to the desired state configuration\u0026rsquo;s magic, a simple command deployed my full NAS solution :\nsudo salt \u0026#39;mynas.local.lan\u0026#39; state.highstate I\u0026rsquo;m not going to share every states I used, just because some are pretty obvious and some others are more related to personal taste.\nBasicaly what has been done is :\nRemove alarmuser Install netatalk with timemachine support Manage Hard disk power (https://wiki.archlinux.org/index.php/Hdparm#Power_management_configuration) Install cronie / set timezone / change anacron tab for job execution during night Install samba + custom configuration and optimizations Install + configure SSMTP (email sending) Optimizations for a20 soc Install and configure smartmontools Install and configure NFS Set custom mount option / custom io scheduler Set cpufreq to ondemand Install some sysadmin tools : bash settings, lsof, unzip\u0026hellip; Install + configure regain install + configure vsftp Create users Set network optimization Performance tuning Based on the following forum\u0026rsquo;s thread which was very instructive, I kept the optimizations explained below.\nhttp://forum.lemaker.org/thread-15543-1-1.html\nInitial Calibration Without any modification on the base Archlinux install, I had:\nhdparm -t -T /dev/sda : around 139 Mb/s FTP transfert (vsftpd) : 18 Mb/s read Samba : 11.3 Mb/s read and 12.0 Mb/s write All tests have been done with a CAT5 cable on the same switch.\nTuning operations Force ethernet IRQ to CPU 1:\n$(cat /proc/interrupts | grep eth0 | cut -f 2 -d \u0026#34;:\u0026#34; | tr -d \u0026#34; \u0026#34;) # give 2 echo 2 \u0026gt; /proc/irq/48/smp_affinity Tune CPU frequency scaling:\necho ondemand \u0026gt; /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor echo 960000 \u0026gt; /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq echo 528000 \u0026gt; /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq echo 1 \u0026gt; /sys/devices/system/cpu/cpufreq/ondemand/io_is_busy echo 25 \u0026gt; /sys/devices/system/cpu/cpufreq/ondemand/up_threshold echo 10 \u0026gt; /sys/devices/system/cpu/cpufreq/ondemand/sampling_down_factor Adjust TCP stack buffers and properties:\nsysctl -w net/core/rmem_max=8738000 sysctl -w net/core/wmem_max=6553600 sysctl -w net/ipv4/tcp_rmem=\u0026#34;8192 873800 8738000\u0026#34; sysctl -w net/ipv4/tcp_wmem=\u0026#34;4096 655360 6553600\u0026#34; sysctl -w vm/min_free_kbytes=65536 sysctl -w net.ipv4.tcp_window_scaling=1 sysctl -w net.ipv4.tcp_timestamps=1 sysctl -w net.ipv4.tcp_sack=1 sysctl -w net.ipv4.tcp_no_metrics_save=1 Finaly, set a bigger queue on eth0:\nip link set eth0 txqueuelen 10000 Results FTP transfert (vsftpd) : 77 Mb/s read Samba : 24.1 Mb/s read and 20.3 Mb/s write While transferring a file, the smbd process is taking 100% of one CPU core. As one file transfer seems to use only one thread, the cpu need to be overclocked to get better performances, but A20 does not easily support large overclocking.\nNFS being more performant, I tested it (nfsv4, ports 2049 and 111 must be open on the firewall, both for TCP and UDP)\nResult :\n57.3 Mb/s read (I did not benchmarked write speed). Some of used Saltstack states I said earlier, I\u0026rsquo;m not sharing all my states, but the following ones are more related to previously mentioned tuning.\nSD card optimization sls file:\n# define sdcard optimized mounting option for root fs on sdcards or emm flash /: mount.mounted: - device: {{ grains[\u0026#39;rootfs\u0026#39;] }} - fstype: ext4 - opts: defaults,async,barrier=0,commit=100,noatime,nodiratime,errors=remount-ro - dump: 0 - pass_num: 1 # set default IO sceduler to deadline for sdcard # deadline scheduler could group small accesses to lesser sdcard latency /etc/udev/rules.d/60-schedulers.rules: file.managed: - source: salt://sdcard_optim/60-schedulers.rules - user: root - group: root - mode: 644 60-schedulers.rules file:\n# set deadline scheduler for sdcard ACTION==\u0026#34;add|change\u0026#34;, KERNEL==\u0026#34;mmcblk[0-9]\u0026#34;, ATTR{queue/scheduler}=\u0026#34;deadline\u0026#34; Gigabit optimization sls file:\n/etc/sysctl.d/10-iptuning.conf: file.managed: - source: salt://gbnetoptim/10-iptuning.conf - user: root - group: root - mode: 644 /etc/udev/rules.d/60-custom-txqueuelen.rules: file.managed: - source: salt://gbnetoptim/60-custom-txqueuelen.rules - user: root - group: root - mode: 644 gbnetoptim_reload_udev: cmd.run: - name: udevadm control --reload-rules gbnetoptim_change_txqueuelen: cmd.run: - name: ip link set eth0 txqueuelen 10000 gbnetoptim_reload_sysctl: cmd.run: - name: sysctl --system file 10-iptuning.conf :\nnet.core.rmem_max = 8738000 net.core.wmem_max = 6553600 net.ipv4.tcp_rmem = 8192 873800 8738000 net.ipv4.tcp_wmem = 4096 655360 6553600 net.ipv4.tcp_timestamps = 0 # less CPU usage on small arm soc net.ipv4.tcp_window_scaling = 1 net.ipv4.tcp_sack = 1 net.ipv4.tcp_no_metrics_save = 1 vm.min_free_kbytes=65536 60-custom-txqueuelen.rules file:\nKERNEL==\u0026#34;eth[0,1]\u0026#34;, RUN+=\u0026#34;/sbin/ip link set %k txqueuelen 10000\u0026#34; KERNEL==\u0026#34;eth[0,1]\u0026#34;, RUN+=\u0026#34;/sbin/ip link set %k txqueuelen 10000\u0026#34; A20 Cpu optimizations sls file:\n# set default IO sceduler to deadline for sdcard # deadline scheduler could group small accesses to lesser sdcard latency /etc/udev/rules.d/65-schedulers.rules: file.managed: - source: salt://a20_optim/65-schedulers-sata.rules - user: root - group: root - mode: 644 /etc/systemd/system/a20_optim.service: file.managed: - source: salt://a20_optim/a20_optim.service - user: root - group: root - mode: 644 a20_optim_reload_systemd: cmd.run: - name : systemctl daemon-reload a20_optim: service.running: - enable: True a20_reload_udev: cmd.run: - name: udevadm control --reload-rules file 65-schedulers-sata.rules:\n# set deadline scheduler for sata (best perf for a20) ACTION==\u0026#34;add|change\u0026#34;, KERNEL==\u0026#34;sd[a-z]\u0026#34;, ATTR{queue/scheduler}=\u0026#34;deadline\u0026#34; file a20_optim.service:\n[Unit] Description=a20 optimizations service After=network.target [Service] Type=oneshot # set lower and higher cpu freq ExecStart=/bin/sh -c \u0026#34;echo 528000 \u0026gt;/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq\u0026#34; ExecStart=/bin/sh -c \u0026#34;echo 960000 \u0026gt;/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq\u0026#34; # avoid cpu detected as idle when there is IO wait (faster transferts) ExecStart=/bin/sh -c \u0026#34;echo 1 \u0026gt; /sys/devices/system/cpu/cpufreq/ondemand/io_is_busy\u0026#34; # tune ondemand to be more reactive ExecStart=/bin/sh -c \u0026#34;echo 25 \u0026gt; /sys/devices/system/cpu/cpufreq/ondemand/up_threshold\u0026#34; ExecStart=/bin/sh -c \u0026#34;echo 10 \u0026gt; /sys/devices/system/cpu/cpufreq/ondemand/sampling_down_factor\u0026#34; # handle network interface IRQ via cpu1 (cpu0 handle sata) ExecStart=/bin/sh -c \u0026#34;echo 2 \u0026gt; /proc/irq/48/smp_affinity\u0026#34; RemainAfterExit=yes [Install] WantedBy=multi-user.target ","permalink":"https://www.bluemind.org/linux-nas-bananapi/","summary":"\u003cp\u003eSince a few years now, I used to use a Sheevaplug as a NAS (see \u003ca href=\"/linux-sheevaplug-perfect-nas/\"\u003e/linux-sheevaplug-perfect-nas\u003c/a\u003e and \u003ca href=\"/linux-sheevaplug-perfect-nas-reloaded/\"\u003e/linux-sheevaplug-perfect-nas-reloaded\u003c/a\u003e). The Sheevaplug was running fine, but I needed some more processing power and memory to make my document search as well as SMB file copy faster and host a saltstack master.\u003c/p\u003e\n\u003cp\u003eThis install has been made with custom saltstack states from another machine before transforming itselft in my saltstack master.\u003c/p\u003e\n\u003ch2 id=\"base-install\"\u003eBase install\u003c/h2\u003e\n\u003ch3 id=\"archlinux\"\u003eArchlinux\u003c/h3\u003e\n\u003cp\u003eInstalling Archlinux on a Bananapi was not officialy supported (at time where I did the install, 1 year ago), but as the hardware is very similar to cubieboard 2, it just needed a dedicated uBoot. Hopefully, someone did it :\u003c/p\u003e","title":"Another NAS with a BananaPI"},{"content":" Yet another homemade portable gaming console ! This one is the sequel to the first one I built. On the first build, I used an Odroid-w (pi clone) and a brand new GameBoy case.\nFor this new project, I wanted something more powerfull to run N64, Dreamcast and PSX games, but also some native linux games\u0026hellip; And there was (is ?) not a lot of low power consumption with sufficient CPU+GPU for that : I chose an Odroid C0.\nMoreover, instead of using and transforming an existing case, I used a 3d printed one designed by myself : optimized dimensions and form factor ;)\nI want to thanks the Odroid community, in particular Meveric for its debian distribution and Odroid optimized packages.\nUsed components Here is a list of all components I used for this build.\nMain parts: Odroid C0 8Gb Emmc module 128 Gb MicroSD XC (SanDisk Ultra, XC I, class 10) 3.5\u0026quot; NTSC/PAL TFT Display A 4x6cm prototype PCB board (ebay) Audio Parts : Stereo 2.8W Class D audio amp 2 PSP 2000/3000 speakers (ebay) A cheap USB sound card with a small USB cable Battery Parts: 2 lipo batteries : keeppower 16650 3.7v 2500mahprotected (important !) 2 MOLEX connectors, 50079-8100 (Ebay) 2 MOLEX receptacle, 51021-0200 (Ebay) Control parts : 12 Soft Tactile Buttons (8mm) 4 tactile button switches (6mm) 2 PSP 1000 Analog sticks (Ebay) 1 Analog multiplexer MC14051BCL (Ebay) Cooling parts : 2 PS3 GPU copper heatsink (Ebay) 4 15x15mm copper heatsink (Ebay) Some 1mm Thermal Pad (Ebay) Some Silicon Thermal paste (ebay) Various other electronic parts: A 3mm blue led (ebay) Some wires from an old IDE ribbon cable Some breadboard connection wires (ebay) 3 resistors Decoration parts: Some Nail polish templates for colors (black, yellow, red, green, blue). Found on eBay. 200, 600 and 1200 sand paper XTC 3D White statin Spray paint Anticipated power consumption The main sources of power drain are the Odroid C0, the display and the audio system (soundcard + audio amp). Before starting, I measured the consumption of these 3 components :\nOdroid C0 : 200-400 mAh depending on CPU and GPU usage Audio system : 310 mAh Display : 420 mAh It\u0026rsquo;s a total of 1130 mAh at 5v, so 5650 mAh / hour. The batteries I used are (at least) 3.7v x 5000 mAh : it\u0026rsquo;s 18500 mAh.\nThe console should (and I confirm, actually) last more than 3h in all cases.\nSome early questions and answers: Why this display with such poor resolution ?\nSeveral reasons for that: low power, real 60 fps, easy wiring (cvbs, so only 1 wire), blurry like old TV which makes an cool hardware anti-aliasing ;)\nWhy using cylindrical batteries ?\nIt\u0026rsquo;s more a matter of space optimization regarding the capacity I wanted. Using a more classical flat battery would have forced to make a case deeper than 2cm. Thought, I have to admit that it was my first intention.\nWhy using a prototype board to mount additional components ?\nThe goal was to easily mount all the components as one unique motherboard\u0026hellip; and I can actually say, it was usefull !\nWhy the need for an analog multiplexer ?\nThe Odroid C0 provides only 2 analog inputs, and one is already used to report the battery level (unfortunately, not that useful, see below). Thus only 1 analog input was available for a total of 4 analog axis (2 thumb sticks with 2 directions each). The only way of reading 4 analog axis with one analog input was a multiplexer. And fortunately, the Odroid C0 has enough digital pins to use 2 of them for analog channels switching.\nEmmc module and MicroSD ?\nThe emmc is really fast regarding microSD. It allows the console to boot in few seconds even with Xorg, a window manager and emulationstation with lots of games.\nI use the emmc for the OS and the microSD for the games and video previews.\n3D model and printing for the case Th console case has been modeled with Freecad. I designed it specifically for this project and the very specific size of the motherboard and all components. It was my first 3D model and first 3D print, so it may contains errors\u0026hellip;\nHowever, the Freecad files are available on github and STL files are freely distributed on thingverse.\nOverview of all printed pieces:\nFront internal (black points are marks to make holes for skewing):\nBack internal. You can see batteries space at the bottom and some striations for CPU + GPU thermal dissipation:\nThe whole size is nearly the same as a Nintendo DS. It may not be obvious, but using dimensions of a well known console allow to find good and cheap protection cases. As you can see on photos later, I used a NDS case to protect my GamOdroid C0, which I found for a few euros (2 or 3 if I remember well).\nTo obtain a nice finish, I first used 600 and 1200 sand paper on all parts.\nThen I used a product which name is XTC-3d. It\u0026rsquo;s awesome and give a nice brilliant finish\u0026hellip; but still not a good enough finish for me ;). I used some sand paper again (1200) before using a white satin aerosol painting. This gave me the finish you can see on photos below.\nFor small parts like buttons and dpad, I used some nail polish. It\u0026rsquo;s very cheap and actually provided great brilliant finish. I finalized buttons and dpad with some transparent nail varnish to protect colors (buttons are the most used part of the console)\nFinished result :\nHardware Assembly My goal was to build a one piece motherboard in order to make it more robust and easier to put inside the case.\nI also built small boards for buttons, DPAD and start + select buttons (sorry, photos are missing).\nDisplay hack The hack is roughly the same as the one I did for my Retroboy console. However, there was some differences on the connector side : V-in and composite output was reversed this time.\nHere is the original display, as found on Adafruit website :\nI first removed the white connector, then I wired the V-in directly to the voltage regulator output and added two wires for powering through one of the Odroid 5v pin:\nSound card I chose a cheap USB sound card with a wire between the board and the USB connector. It was important because it was easier to unsolder.\nI started to dismantle wires, connectors and then re-drilled holes. I prepared the Odroid board by adding pins to the first USB connector.\nFinally, I soldered the sound card directly on the pins :\nExtension board with USB port I put the extension board just below the USB sound card. I first soldered a USB connector, then I wired it to the second Odroid USB connector through the extension board.\nNote that I also soldered the extension board to the Odroid motherboard to make the whole thing more robust.\nFinishing audio on the extension board Having a sound card with analog output is nice, but with a 3.5 audio jack and a good amp to drive speakers is better !\nThat was exactly the next step : wiring and soldering components on the extension board.\nAnalog multiplexer wiring The soldering of this small piece started to add a lot of wires and finished to fill the extension board. I had to use the following: Vdd (Vin), Vss (ground), x (analog output), x0, x1, x2, x3 (analog inputs), A, B (digital switches).\nC was not needed as 2 switches were enough to switch the first 4 outputs.\nVee and INH has been wired to ground\nNote that I made a voltage divider bridge between x (output) and the analog input of the Odroid. This is because the psp analog sticks and MC14051B operate in 5V whereas the Odroid C0 analog input accept a maximum of 1.8v.\nVolume buttons You may have noticed on the previous photo that there were also 2 push buttons on one edge of the extension board.\nI just wired them to GPIO pins to control audio volume:\nStart + Select I used push buttons for start and select buttons. I mounted them on an small additional board together with blue led (for battery monitoring).\nBatteries As indicated before, I used a pair of protected cylindrical lipo batteries. I wired them in parallel to get 5000 mAh.\nI had to solder some wires directly on batteries and add a Molex connector to be able to connect the two wired batteries to the Odroid C0 Lipo connector.\nMounting everything in the case At this time, I had done everything on the hardware side. I started to mount in the front part of the case the display, analog sticks, d-pad, a-b-x-y boards and L1 + R1 buttons.\nThe display is not glued but maintained with two traversal bars. As you will see in the next photo, theses bars allowed me to also block and drive all wires.\nNext step for the front part of the case : adding speakers, start+select buttons board and wiring everything with a common ground… yes at this point, it started to be a mess ;)\nFinal steps before closing: adding a heat-sink, putting L2+R2 buttons and the motherboard in the back part of the case, then soldering everything to GPIO (note also the yellow wire : this is the composite output of the Odroid that go to the display input 1)\nResult of assembly It was time to close the case and check the result;)\nSoftware part I did a script that construct 80% of the system including copy of specific config files. The other 20% are for roms and personal customization.\nIf someone wants to do the same, it should be quite easy to adapt / re-run the script.\nLinux distrib and File system organization Before starting to comment the install script, here are the base install steps I did :\nDeployment of Meveric’s minimal Debian Jessie image on the emmc Creation of two partitions on the 128 Gb micro sd: 4 Gb for save states and the rest for roms (they will be mounted has /mnt/states and /mnt/ressources). I did 2 partitions because I had the intention to create a read-only system excepted for states… but I finaly kept a full RW system (ext4 is robust enough). Creation of a GameOdroid folder in /root and copy the install script and its dependencies Install Script The install script and all dependencies can be found on github. It is organized with functions dedicated for each steps.\nfirst step : prepare the system The first executed function create custom mount points, copy custom fstab and activate tmpfs :\nfunction fstab { echo \u0026#34;fstab and filesystem\u0026#34; mkdir -p /mnt/states mkdir -p /mnt/ressources cp /root/GameOdroid/fstab /etc/fstab sed -i \u0026#34;s/#RAMLOCK=yes/RAMLOCK=yes/\u0026#34; /etc/default/tmpfs sed -i \u0026#34;s/#RAMSHM=yes/RAMSHM=yes/\u0026#34; /etc/default/tmpfs } The custom fstab file allows to change mount options in order to optimize for speed (noatime, discard) and use a small tmpfs partition for /var/log\ntmpfs /var/log tmpfs nodev,nosuid,noatime,size=20M 0 0 After this first function, the system is rebooted, then upgraded and rebooted again:\nfunction uptodate { echo \u0026#34;update\u0026#34; apt-get update apt-get upgrade apt-get dist-upgrade } The final step of this stage is to install all needed base package (function syspackages). Nothing special here excepted two things :\nevilwm : I had to use a window manager because some native games can’t find the native screen resolution without it. I found that evilwm was a very good candidate for the console : very light and invisible with default settings. Antimicro-odroid : it’s a very nice piece of software I did not know about before. It allows to map any keyboard and mouse event to a joypad. Python package evdev : used to configure reicast input not a package, but important to notice : I used a Odroid C1/C0 specific xorg config file given by Meveric (http://oph.mdrjr.net/meveric/other/C1/xorg.conf) Games This part correspond to functions \u0026ldquo;emulators\u0026rdquo;, \u0026ldquo;emulators_glupen64_meveric\u0026rdquo; and nativegames.\nExcepted for Dreamcast games for which I used reicast, all other emulators are part of retroarch :\npcsx-rearmed (PSX) fbalpha (CPS2) gambatte (Gameboy color) gpsp (Gameboy advance) mednafen-pce-fast (Pc-Engine + Cdrom) nestopia (Nes) picodrive (Sega 32X, SegaCD) pocketnes (Snes) genesis-plus-gx (GameGear, Genesis, MasterSystem) mednafen-ngp (Neogeo pocket color) For natives games, I selected those that was enjoyable with a gamepad and was running correctly on the Odroid C0 with a small screen:\nhurrican hcraft frogatto SuperMario War astromenace neverball shmupacabra aquaria Revolt Open JK3 openjazz supertuxkart mars puzzlemoppet opentyrian pushover Game launcher This correspond to the function \u0026ldquo;userinterface\u0026rdquo;.\nInitially, I wanted to use Attract mode. Unfortunately, the implementation of GLES on Odroid C0/C1 seems not to have implemented glBlendEquationSeparateOES() and glBlendFuncSeparateOES() functions\u0026hellip; which are mandatory to compile libFSML\u0026hellip; which in turn is mandatory to compile Attract mode.\nThus, I used the latest EmulationStation version with video preview support. As I wanted to change the default splash screen with a custom one, I had to replace \u0026ldquo;splash_svg.cpp\u0026rdquo; file in \u0026ldquo;EmulationStation/data/converted\u0026rdquo;. This file is a simple C array that contains the bytes of an SVG file.\nDespite the classical configuration of systems, I create a specific one that list two scripts to change the display : internal screen or HDMI (see composite.sh and hdmi.sh scripts)\nSpecific tools This correspond to the function \u0026ldquo;localtools\u0026rdquo;.\nThis is mainly to handle the custom GPIO gamepad. I had to wrote a small program in C that creates a gamepad through linux\u0026rsquo;s uinput and poll GPIO to generate events\u0026hellip; yes, polling and not IRQ based because the SoC does not have enough IRQ to handle all the buttons.\nI named this tool gpio_joypad and the source code is on github. It also handles the analog multiplexer to get left and right analog thumb sticks values.\nBoot config file This correspond to the function \u0026ldquo;bootini\u0026rdquo;.\nThis function consists in copying a customized boot.ini file to the boot partition. The important changes I made are :\nKeeping only two video modes : cvbs480 (activated by default) and vga (commented out). Disabled cec and vpu Modified kernel arguments: \u0026ldquo;cvbsmode=480cvbs\u0026rdquo; to get a 60Hz NTSC resolution instead of 50 Hz PAL \u0026ldquo;max_freq=1824\u0026rdquo; to overclock the SoC (needed for N64 and Dreamcast emulators) \u0026ldquo;quiet loglevel=3 rd.systemd.show_status=false udev.log-priority=3\u0026rdquo; to make the boot as quiet as possible Initially, I wanted to display the splash screen early during the boot process. It is well documented on Odroid wiki. Unfortunately it works only for 720p resolutions :(\nLaunch everything at start This correspond to the function \u0026ldquo;startup\u0026rdquo;\nThe automatic startup of X and Emulationstation at boot consisted in a custom tty1 service in systemd that launch agetty with autologin, a bash profile that launch X when tty variable = tty1 and finaly a xinitrc that start the window manager and Emulationstation.\nThe custom tty1 service (/etc/systemd/system/getty@tty1.service.d/override.conf):\n[Service] ExecStart= ExecStart=-/sbin/agetty --autologin root --noclear %I $TERM The bash /root/.profile :\n# ~/.profile: executed by Bourne-compatible login shells. if [ \u0026#34;$BASH\u0026#34; ]; then if [ -f ~/.bashrc ]; then . ~/.bashrc fi fi if [ \u0026#34;$(tty)\u0026#34; = \u0026#34;/dev/tty1\u0026#34; ] ; then /usr/local/bin/battery.sh \u0026amp; /usr/local/bin/gpio-joypad \u0026amp; startx -- -nocursor 2\u0026gt;\u0026amp;1 \u0026amp; fi mesg n And the /root/.xinitrc\n# a WM is needed some software are correctly sized in full screen # e.g : emulationstation, rvgl evilwm \u0026amp; pid=$! emulationstation.sh \u0026amp; # this allows not to shutdown X when emulation is killed # We want that because we have to kill it after gamelaunch # else it does not reappear on screen (SDL_Createwindow() does never end) wait $pid Note that the bash profile start the joypad driver (gpio_joypad) and the battery monitoring script (battery.sh) before starting X.\nThe battery monitoring script is not very accurate, but I dit not found any way to make a better monitoring to switch on the led on low battery or when charging:\n#!/bin/bash PIN=75 GPIO=/sys/class/gpio ACCESS=$GPIO/gpio$PIN LOWBAT=780 CHARGING=1020 if [ ! -d $ACCESS ] ; then echo $PIN \u0026gt; $GPIO/export echo out \u0026gt; $ACCESS/direction echo 0 \u0026gt; $ACCESS/value fi while true do ADCVAL=$(cat /sys/class/saradc/saradc_ch0) # echo \u0026#34;value : $ADCVAL\u0026#34; # charging if [ $ADCVAL -gt $CHARGING ]; then echo 1 \u0026gt; $ACCESS/value else # low bat if [ $ADCVAL -lt $LOWBAT ]; then echo 1 \u0026gt; $ACCESS/value sleep 1 echo 0 \u0026gt; $ACCESS/value else echo 0 \u0026gt; $ACCESS/value fi fi sleep 2 done Finalize \u0026amp; clean up This correspond to the function \u0026ldquo;optimize_system\u0026rdquo;.\nIn this function, the bash login message is hidden (to make the boot process as silent as possible) and packages cache is cleaned (apt-get clean).\nThere is also two configuration files that are deployed.\nThe custom journald.conf is here to write logs in ram instead of disk (better performance):\n[Journal] Storage=volatile I also created a specific alsa configuration file to add latency and buffers, so most sound stutering are avoided for n64 and dreamcast games:\npcm.!default { type plug slave.pcm \u0026#34;softvol\u0026#34; ttable.0.1 0.8 ttable.1.0 0.8 } pcm.dmixer { type dmix ipc_key 1024 slave { pcm \u0026#34;hw:1,0\u0026#34; period_time 0 period_size 2048 buffer_size 65536 rate 44100 } bindings { 0 0 1 1 } } pcm.dsnooper { type dsnoop ipc_key 1024 slave { pcm \u0026#34;hw:1,0\u0026#34; channels 2 period_time 0 period_size 2048 buffer_size 65536 rate 44100 } bindings { 0 0 1 1 } } pcm.softvol { type softvol slave { pcm \u0026#34;dmixer\u0026#34; } control { name \u0026#34;Master\u0026#34; card 1 } } ctl.!default { type hw card 1 } ctl.softvol { type hw card 1 } ctl.dmixer { type hw card 1 } Emulators specific settings Global Retroarch configuration Despite changing buttons and path, I had to adapt some videos parameters of retroarch (root/.config/retroarch/retroarch.cfg) to optimize performance and better suit the hardware.\nvideo_refresh_rate = \u0026#34;59.950001\u0026#34; video_monitor_index = \u0026#34;0\u0026#34; video_fullscreen_x = \u0026#34;720\u0026#34; video_fullscreen_y = \u0026#34;480\u0026#34; video_vsync = \u0026#34;true\u0026#34; video_threaded = \u0026#34;true\u0026#34; video_force_aspect = \u0026#34;true\u0026#34; Core specific configuration I also did some adjustements on few emulator\u0026rsquo;s cores.\nAllowing 6 buttons for SegaCD and 32X:\npicodrive_input1 = \u0026#34;6 button pad\u0026#34; Changing glupen64 parameters to optimize rendering on the Odroid SoC:\nglupen64-cpucore = \u0026#34;dynamic_recompiler\u0026#34; glupen64-rspmode = \u0026#34;HLE\u0026#34; glupen64-43screensize = \u0026#34;320x240\u0026#34; glupen64-BilinearMode = \u0026#34;standard\u0026#34; Allowing PSX analog joypad support:\npcsx_rearmed_pad1type = \u0026#34;analog\u0026#34; Reicast For the Dreamcast emulator, I used reicast-joyconfig to generate the gamepad config and copied the resulting file to /root/.config/reicast/joy.conf\nI also changed the fullscreen resolution to adapt it to the CVBS display:\n[x11] fullscreen = 1 height = 480 width = 720 Keyboard \u0026amp; mouse mapping for native games Some native games work fine but require either a mouse or a keyboard (esc, enter, space, shift or up/down/left/right keys).\nTo map needed keys to the console gamepad I used antimicro. It\u0026rsquo;s a very nice and easy to use program to map any mouse and keyboard key to any gamepad buttons.\nScraping videos Emulationstation has an integrated scraper for game informations and pictures, but not for videos.\nMoreover, if video previews are supported depending on the chosen themes, they are played throught vlc\u0026hellip; which is not accelerated on Odroid C0/C1 SoC. The consequence is that 320x240@30 fps in h.264 is the biggest playable size.\nI wrote and used a custom script available on github: https://github.com/jit06/gamesdatabase_scraper\nThis script parse Emulationstation gamelist folder and scrap videos from www.gamesdatabase.org\nFinal result : videos \u0026amp; photos [nggallery id=64]\nLessons learned No way to correctly monitor Battery on Odroid C0 (charging, charged, etc.) Too bad to have only 2 mali 450 on the SoC, even with overclock, it is still too slow for a lot of n64 and Dreamcast games Some crash that seems to be related to graphic driver (e.g emulationstation that do not exit properly, hurrican that does not start with the correct resolution from time to time) Not possible to use a proper interrupt based joypad driver, too few IRQ available on the soc Need for a window manager, else no fullscreen size for games nor emulationstation Reicast : seems to emulate the GDRom noise, but I actually find it pretty anoying… ","permalink":"https://www.bluemind.org/gamodroid-c0-odroid-based-portable-retrogaming/","summary":"\u003cp\u003e\u003ca href=\"images/logo_gamodroid_c0.png\"\u003e\u003cimg\n  src=\"images/logo_gamodroid_c0.png\"\n  alt=\"\"\n  class=\"article-image\"\n  loading=\"lazy\"\n\u003e\u003c/a\u003e Yet another homemade portable gaming console ! This one is the sequel to \u003ca href=\"/hardware-linux-retro-boy-portable-gaming-console-odroid-w-gameboy-case/\"\u003ethe first one I built\u003c/a\u003e. On the first build, I used an Odroid-w (pi clone) and a brand new GameBoy case.\u003c/p\u003e\n\u003cp\u003eFor this new project, I wanted something more powerfull to run N64, Dreamcast and PSX games, but also some native linux games\u0026hellip; And there was (is ?) not a lot of low power consumption with sufficient CPU+GPU for that : I chose an \u003ca href=\"http://www.hardkernel.com/main/products/prdt_info.php?g_code=G145326484280\"\u003eOdroid C0\u003c/a\u003e.\u003c/p\u003e","title":"GamOdroiD C0 : an Odroid based portable retrogaming"},{"content":"As I\u0026rsquo;m using monitorix to monitor all my servers, I naturally did a dedicated state for saltstack. I also coupled this with a state to install Anything-sync-Daemon which is an Archlinux\u0026rsquo;s AUR Package that use tmpfs together with overlayfs to reduce wear on physical disk (data are stored to memory and synchronized on a regular basis on disk).\nI also use states to add specific monitoring for some services like docker.\nBuild scripts Monitorix and Anything-sync-daemon are both available from AUR on Archlinux. I just use some small scripts to quickly build the binary packages without any fingerprint on the host used to build. The two scripts a very similar and are just used to save few lines of shell commands ( yes, I\u0026rsquo;m that lazy\u0026hellip;)\nAnything-sync-daemon build-asd.sh :\n#!/bin/bash WORKDIR=\u0026#34;asd\u0026#34; mkdir $WORKDIR cd $WORKDIR echo \u0026#34;\u0026#34; echo \u0026#34;------ get sources package... -------\u0026#34; git clone https://aur.archlinux.org/anything-sync-daemon.git echo \u0026#34;\u0026#34; echo \u0026#34;------ building... -------\u0026#34; cd anything-sync-daemon makepkg echo \u0026#34;\u0026#34; echo \u0026#34;------ finalizing... -----\u0026#34; mv anything-sync-daemon-*-any.pkg.tar.xz ../../ cd ../../ rm -Rf asd Monitorix build_monitorix.sh:\n#!/bin/bash WORKDIR=\u0026#34;monitorix\u0026#34; mkdir $WORKDIR cd $WORKDIR echo \u0026#34;\u0026#34; echo \u0026#34;------ get sources package... -------\u0026#34; git clone https://aur.archlinux.org/monitorix.git echo \u0026#34;\u0026#34; echo \u0026#34;------ building... -------\u0026#34; cd monitorix makepkg echo \u0026#34;\u0026#34; echo \u0026#34;------ finalizing... -----\u0026#34; mv monitorix-*-any.pkg.tar.xz ../../ cd ../../ rm -Rf monitorix States The states below basicaly handle dependencies, packages installation and custom configuration file deployment.\nAnything-sync-daemon The ASD state is pretty simple:\n# needed package for asd neededpkgs: pkg.installed: - pkgs: - procps-ng - rsync # deploy custom package /var/cache/pacman/pkg/anything-sync-daemon-5.76-1-any.pkg.tar.xz: file.managed: - source: salt://anything-sync-daemon/anything-sync-daemon-5.76-1-any.pkg.tar.xz - user: root - group: root - mode: 644 pacman --noconfirm -U /var/cache/pacman/pkg/anything-sync-daemon-5.76-1-any.pkg.tar.xz: cmd.run: - creates: /usr/bin/anything-sync-daemon # ensure service is running asd: service: - running - watch: - file: /etc/asd.conf # deploy custom config /etc/asd.conf: file: - managed - source: salt://anything-sync-daemon/asd.conf - user: root - group: root - mode: 644 /etc/modules-load.d/overlay.conf: file.managed: - source: salt://anything-sync-daemon/overlay.conf - user: root - group: root - mode: 644 The \u0026ldquo;overlay.conf \u0026quot; is used to activate the overlay kernel module at boot.\nThe reason I use a custom configuration for ASD file (asd.conf) is to allow adding directories to sync. In fact, I\u0026rsquo;m using ASD both for monitorix and docker, so my custom config file contains markers to easily adapt the config file in other states (eg. \u0026ldquo;# monitorix start\u0026rdquo; and \u0026ldquo;# monitorix end\u0026rdquo;)\nThe pushed asd.conf file:\n# # /etc/asd.conf # # For documentation, refer to the asd man page ## WARNING Do NOT edit anything in this file while asd is running! ## To protect data from corruption, in the event that you do make an edit ## while asd is active, any changes made will be applied the next time ## you start-up asd. # Define the target(s) directories in the WHATTOSYNC array # Do NOT define a file! These MUST be directories with an absolute path! # # Note that the target DIRECTORIES and all subdirs under them will be included. # In other words, this is recursive. # # Below is an example to whet your appetite. #WHATTOSYNC=(\u0026#39;/srv/http\u0026#39; \u0026#39;/var/lib/monitorix\u0026#39; \u0026#39;/foo/bar\u0026#39;) WHATTOSYNC=( \u0026#39;/var/log\u0026#39; # monitorix start # monitorix end # docker start # docker end ) # Define where data will reside in tmpfs. # This location must be mounted to tmpfs and MUST be writable and executable. # # If using bleachbit, do NOT invoke it with the \u0026#39;--clean system.tmp\u0026#39; switch or # you will remove a key dot file (.foo) from /tmp that asd needs to keep track # of sync status. # # Note that using a value of \u0026#39;/dev/shm\u0026#39; can cause problems with systemd\u0026#39;s # NAMESPACE spawning only when users enable the overlayfs option. # # Use NO trailing backslash! VOLATILE=\u0026#34;/tmp\u0026#34; # Uncomment and set to yes to use an overlayfs instead of a full copy to reduce # the memory costs and to improve sync/unsync operations. # # You must modprobe either the \u0026#39;overlayfs\u0026#39; or \u0026#39;overlay\u0026#39; module prior to running asd if # you enable this option. Distros running the linux kernel version \u0026gt;=3.18.0 are likely # using the \u0026#39;overlay\u0026#39; module while some distros shipping older kernels, notably Ubuntu # provide the older version of this technology which is provided in the \u0026#39;overlayfs\u0026#39; # module not \u0026#39;overlay\u0026#39; module. USE_OVERLAYFS=\u0026#34;yes\u0026#34; # Uncomment and set to no to completely disable the crash recovery feature of asd. # # The default is to create crash recovery backups if the system is ungracefully # powered-down due to a kernel panic, hitting the reset switch, battery going # dead, etc. Some users keep very diligent backups and don\u0026#39;t care to have this # feature enabled. #USE_BACKUPS=\u0026#34;yes\u0026#34; As shown in the monitorix state below, I use the following lines to add the corresponding ASD line in asd.conf file from the monitorix state:\nmonitorix_asd.conf: file.blockreplace: - name: /etc/asd.conf - marker_start: \u0026#34;# monitorix start\u0026#34; - marker_end: \u0026#34;# monitorix end\u0026#34; - content: \u0026#34;\u0026#39;/var/lib/monitorix\u0026#39; \u0026#39;/srv/http/monitorix\u0026#39;\u0026#34; Monitorix The monitorix state install all required softwares and a custom configuration file, again, to allow customization depending on other states I have (eg: docker monitoring).\nThe monitorix state # Docker image building template include: - cron monitorixpkgs: pkg.installed: - pkgs: - mesa-libgl - perl - perl-cgi - perl-mailtools - perl-mime-lite - perl-libwww - perl-dbi - perl-xml-simple - perl-config-simple - perl-config-general - rrdtool - perl-http-server-simple /var/cache/pacman/pkg/monitorix-3.9.0-1-any.pkg.tar.xz: file: - managed - source: salt://monitorix/monitorix-3.9.0-1-any.pkg.tar.xz - user: root - group: root - mode: 644 pacman --noconfirm -U /var/cache/pacman/pkg/monitorix-3.9.0-1-any.pkg.tar.xz: cmd.run: - creates: /usr/bin/monitorix /etc/monitorix/monitorix.conf: file.managed: - source: salt://monitorix/monitorix.conf - user: root - group: root - mode: 644 monitorix_title_conf: file.blockreplace: - name: /etc/monitorix/monitorix.conf - marker_start: \u0026#34;# title start\u0026#34; - marker_end: \u0026#34;# title end\u0026#34; - content: \u0026#34;title = {{ grains[\u0026#39;host\u0026#39;] }} monitoring\u0026#34; monitorix_hostname_conf: file.blockreplace: - name: /etc/monitorix/monitorix.conf - marker_start: \u0026#34;# hostname start\u0026#34; - marker_end: \u0026#34;# hostname end\u0026#34; - content: \u0026#34;hostname = {{ grains[\u0026#39;host\u0026#39;] }}\u0026#34; monitorix_asd.conf: file.blockreplace: - name: /etc/asd.conf - marker_start: \u0026#34;# monitorix start\u0026#34; - marker_end: \u0026#34;# monitorix end\u0026#34; - content: \u0026#34;\u0026#39;/var/lib/monitorix\u0026#39; \u0026#39;/srv/http/monitorix\u0026#39;\u0026#34; monitorix_email_address_conf: file.blockreplace: - name: /etc/monitorix/monitorix.conf - marker_start: \u0026#34;# from email address start\u0026#34; - marker_end: \u0026#34;# from email address end\u0026#34; - content: \u0026#34;from_address = {{ grains[\u0026#39;host\u0026#39;] }}@domain.ext\u0026#34; monitorix: service: - running - enable: True - watch: - file: /etc/monitorix/monitorix.conf My monitorix.conf file is based on the original one, on which I changed some parts to be able to change or add parameters from saltstack states with markers.\nAs an example, below are parts of the monitorix configuration file where I put markers for my \u0026ldquo;monitorix for docker\u0026rdquo; state.\nThe FS part # FS graph # ----------------------------------------------------------------------------- \u0026lt;fs\u0026gt; \u0026lt;list\u0026gt; # fs list start 0 = / # fs list stop \u0026lt;/list\u0026gt; \u0026lt;desc\u0026gt; \u0026lt;/desc\u0026gt; \u0026lt;devmap\u0026gt; # fs devmap start / = mmcblk0p2 # fs devmap stop \u0026lt;/devmap\u0026gt; rigid = 2, 0, 2, 0 limit = 100, 1000, 100, 1000 \u0026lt;alerts\u0026gt; / = 3600, 75, /usr/local/bin/alert_diskspace_root # fs alerts start 1 # fs alerts stop 1 \u0026lt;/alerts\u0026gt; \u0026lt;/fs\u0026gt;\u0026lt; ; As you can see, I use a custom script for disk space alert handling (which are also deployed as a state), as explained in the official monitorix documentation\nThe Du part # DU graph # ----------------------------------------------------------------------------- \u0026lt;du\u0026gt; list = System, Users \u0026lt;desc\u0026gt; 0 = /tmp, /var/log, /var/lib/monitorix # du desk start 1 # du desk end 1 \u0026lt;/desc\u0026gt; \u0026lt;dirmap\u0026gt; /var/spool/mail = Mail boxes /var/spool/mqueue = Mail queue \u0026lt;/dirmap\u0026gt; graphs_per_row = 2 rigid = 0 limit = 100 \u0026lt;/du\u0026gt; The process part # PROCESS graph # ----------------------------------------------------------------------------- \u0026lt;process\u0026gt; \u0026lt;list\u0026gt; # process list start 1 0 = sshd # process list end 1 \u0026lt;/list\u0026gt; \u0026lt;desc\u0026gt; # process desc start 1 # process desc end 1 # process desc start 2 # process desc end 2 # process desc start 3 # process desc end 3 # process desc start 4 # process desc end 4 \u0026lt;/desc\u0026gt; rigid = 0, 0, 0, 0 limit = 1000, 1000, 1000, 1000 \u0026lt;/process\u0026gt; Monitorix state for docker monitoring I use a \u0026ldquo;sub monitorix state\u0026rdquo; to add docker monitoring for hosts that run docker containers. This state is named \u0026ldquo;4docker.sls\u0026rdquo; and include the monitorix state. So adding monitoring support for docker host is easily done with a call like :\nsalt \u0026#39;mydockerhost.local.lan\u0026#39; state.apply monitorix.4docker The 4docker.sls file :\ninclude: - monitorix - custom_bin # activate disk monitoring add_docker_disk_check: file.line: - name: /etc/monitorix/monitorix.conf - mode: replace - match: disk = - content: disk = y config-4docker-fs-1: file.blockreplace: - name: /etc/monitorix/monitorix.conf - marker_start: \u0026#34;# fs list start\u0026#34; - marker_end: \u0026#34;# fs list stop\u0026#34; - content: 0 = /,/var/lib/docker/volumes config-4docker-fs-2: file.blockreplace: - name: /etc/monitorix/monitorix.conf - marker_start: \u0026#34;# fs alerts start 1\u0026#34; - marker_end: \u0026#34;# fs alerts stop 1\u0026#34; - content: /var/lib/docker/volumes = 7200, 80, /usr/local/bin/alert_diskspace_dockervolumes config-4docker-du-1: file.blockreplace: - name: /etc/monitorix/monitorix.conf - marker_start: \u0026#34;# du desk start 1\u0026#34; - marker_end: \u0026#34;# du desk end 1\u0026#34; - content: 1 = /var/lib/docker/aufs, /var/lib/docker/image config-4docker-process-1: file.blockreplace: - name: /etc/monitorix/monitorix.conf - marker_start: \u0026#34;# process list start 1\u0026#34; - marker_end: \u0026#34;# process list end 1\u0026#34; - content: 0 = sshd, docker config-4docker-process-desc-1: file.blockreplace: - name: /etc/monitorix/monitorix.conf - marker_start: \u0026#34;# process desc start 1\u0026#34; - marker_end: \u0026#34;# process desc end 1\u0026#34; - content: docker = Docker As a example of result, the file monitoring graph is like the following :\n","permalink":"https://www.bluemind.org/saltstack-state-monitorix-asd-archlinux/","summary":"\u003cp\u003eAs I\u0026rsquo;m using \u003ca href=\"http://www.monitorix.org/\"\u003emonitorix\u003c/a\u003e to monitor all my servers, I naturally did a dedicated state for saltstack. I also coupled this with a state to install \u003ca href=\"https://wiki.archlinux.org/index.php/Anything-sync-daemon\"\u003eAnything-sync-Daemon\u003c/a\u003e which is an Archlinux\u0026rsquo;s \u003ca href=\"https://aur.archlinux.org/packages/\"\u003eAUR Package\u003c/a\u003e that use \u003ca href=\"https://wiki.archlinux.org/index.php/Tmpfs\"\u003etmpfs\u003c/a\u003e together with \u003ca href=\"https://en.wikipedia.org/wiki/OverlayFS\"\u003eoverlayfs\u003c/a\u003e to reduce wear on physical disk (data are stored to memory and synchronized on a regular basis on disk).\u003c/p\u003e\n\u003cp\u003eI also use states to add specific monitoring for some services like \u003ca href=\"https://www.docker.com\"\u003edocker\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"build-scripts\"\u003eBuild scripts\u003c/h2\u003e\n\u003cp\u003eMonitorix and Anything-sync-daemon are both available from AUR on Archlinux. I just use some small scripts to quickly build the binary packages without any fingerprint on the host used to build. The two scripts a very similar and are just used to save few lines of shell commands ( yes, I\u0026rsquo;m that lazy\u0026hellip;)\u003c/p\u003e","title":"Saltstack : state for monitorix + ASD to monitor docker (Archlinux)"},{"content":"All my servers are small Arm SBCs that use SDCards or eMMC for storage. For all of them, I use the same optimization state that basically do 2 things :\nChange commit and barrier settings of ext4 (with a proper backup-ed power system) Set the scheduler to deadline, which is the more efficient one for my usage The state:\n# define sdcard optimized mounting option for root fs on sdcards or emm flash /: mount.mounted: - device: {{ grains[\u0026#39;rootfs\u0026#39;] }} - fstype: ext4 - opts: defaults,async,barrier=0,commit=100,noatime,nodiratime,errors=remount-ro - dump: 0 - pass_num: 1 # set default IO sceduler to deadline for sdcard # deadline scheduler could group small accesses to lesser sdcard latency /etc/udev/rules.d/60-schedulers.rules: file.managed: - source: salt://sdcard_optim/60-schedulers.rules - user: root - group: root - mode: 644 As you can see, I use a custom grains to determine the root partition, because I have several servers with different configurations.\nHere is my \u0026ldquo;rootfs\u0026rdquo; grains, stored in /states/base/_grains/rootfs.py:\n#!/usr/bin/python import subprocess def function(): \u0026#39;\u0026#39;\u0026#39; Return the rootfs partition \u0026#39;\u0026#39;\u0026#39; grains = {} command = (\u0026#39;df -h | grep \\\u0026#39;\\/$\\\u0026#39; | awk \\\u0026#39;{ print $1 }\\\u0026#39;\u0026#39;) p = subprocess.Popen(command, universal_newlines=True,shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE) grains[\u0026#39;rootfs\u0026#39;] = p.stdout.read() retcode = p.wait() return grains The \u0026ldquo;60-schedulers.rules\u0026rdquo; files is a simple udev order to force the deadline scheduler on all SDCards :\n# set deadline scheduler for sdcard ACTION==\u0026#34;add|change\u0026#34;, KERNEL==\u0026#34;mmcblk[0-9]\u0026#34;, ATTR{queue/scheduler}=\u0026#34;deadline\u0026#34; ","permalink":"https://www.bluemind.org/saltstack-state-sdcard-flash-storage-optimization-arm-sbc/","summary":"\u003cp\u003eAll my servers are small Arm SBCs that use SDCards or eMMC for storage. For all of them, I use the same optimization state that basically do 2 things :\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eChange commit and barrier settings of ext4 (with a proper backup-ed power system)\u003c/li\u003e\n\u003cli\u003eSet the scheduler to deadline, which is the more efficient one for my usage\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe state:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# define sdcard optimized mounting option for root fs on sdcards or emm flash\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e/\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003emount.mounted\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e - \u003cspan style=\"color:#f92672\"\u003edevice\u003c/span\u003e: {{ \u003cspan style=\"color:#ae81ff\"\u003egrains[\u0026#39;rootfs\u0026#39;] }}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e - \u003cspan style=\"color:#f92672\"\u003efstype\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eext4\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e - \u003cspan style=\"color:#f92672\"\u003eopts\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003edefaults,async,barrier=0,commit=100,noatime,nodiratime,errors=remount-ro\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e - \u003cspan style=\"color:#f92672\"\u003edump\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e - \u003cspan style=\"color:#f92672\"\u003epass_num\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# set default IO sceduler to deadline for sdcard\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# deadline scheduler could group small accesses to lesser sdcard latency\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e/etc/udev/rules.d/60-schedulers.rules\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003efile.managed\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e - \u003cspan style=\"color:#f92672\"\u003esource\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003esalt://sdcard_optim/60-schedulers.rules\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e - \u003cspan style=\"color:#f92672\"\u003euser\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eroot\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e - \u003cspan style=\"color:#f92672\"\u003egroup\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eroot\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e - \u003cspan style=\"color:#f92672\"\u003emode\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e644\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAs you can see, I use a custom grains to determine the root partition, because I have several servers with different configurations.\u003c/p\u003e","title":"Saltstack : state for SDCard / flash storage optimization (Arm SBC)"},{"content":"The story began some years ago, when I built from scratch a \u0026ldquo;true\u0026rdquo; fullsized arcade cabinet (see the gallery). This Mame Cab was pretty cool, with nice arcade feeling : CRT screen, optical guns (ps 1), trackball, real coin door.\nThe problem was the size of the beast : HxLxD was 190x80x120 cm. It was taking a lot of space\u0026hellip; Space that I needed to create a new study area.\nSo, I sold my \u0026ldquo;old\u0026rdquo; Mame Cab and started to build a bartop which is a lot smaller\u0026hellip;\nFeatures The Bartop is able to run many arcade games and uses Hyperspin as frontend:\nLaser disc with Daphne Misc Arcade with Mame Sega Model 2 with Nebula Two Sega Model 3 games with Supermodel ZN1 with Zinc Some pinballs with Futur Pinball Main features are :\nBacklit marquee and bottom, for a cool look in the dark \u0026ldquo;Always on\u0026rdquo; backlit buttons 19 inch DVI display to get a decent size with a 4:3 ratio Stereo sound, with adjustable volume, bass and treble Light Gun support throught Wiimote Side buttons for pinball tables Standard C14 Power plug with on/off switch Accessible usb ports (for wifi, keyboard, etc.) Hardware part Components Gettings nice emulators and good looking frontend (which is a matter of personnal taste) often require M$ Windows\u0026hellip; and so a full featured PC with good performances. I don\u0026rsquo;t usualy use Windows at home, but I really put emulators and frontend as priority !\nThus, all electronics parts are \u0026ldquo;simple\u0026rdquo; PC parts or interfaces.\nPC (I already had all of this, except the monitor and the ssd):\nMini ITX motherboard : Jetway nc62k CPU : Atlhon XP 64 6000+ together with Akasa AK-861CU AMD Low Noise Cooler 2 Gb Ram DDR2 800 DVI Cable 19 inch LCD Monitor : LG L1972H (found on www.priceminister.com) A small 120W power supply (for PC, Leds, audio amplifier and buttons interfaces) Samsung SSD (840 evo) Electronics parts:\nPlayer 1 \u0026amp; 2 start buttons : found on ebay, sold by \u0026ldquo;procardetails\u0026rdquo; Dolphin Bar (for wiimote) : can be found on ebay or Amazon Two player Led Arcade Game kit: found on ebay, sold by \u0026ldquo;amye-shop\u0026rdquo; C14 Power plug with On/Off switch : found on ebay, sold by \u0026ldquo;gagaoutleteb\u0026rdquo; standard 12V 80x80mm PC fan A wiimote + LightGun Furnitures:\nA DIY bartop kit + plexiglass parts, found on ebay, sold by \u0026ldquo;djwillione\u0026rdquo; 12V Audio amplifier + Power + Oval Speaker, same seller White corner \u0026amp; straight lugs, local DIY shop Cupboard\u0026rsquo;s door magnets and hinges, local DIY shop 5V leds ruber for TV, local DIY shop (link) Small HETTICH plastic assembly parts (link) Decorations:\nA4 side stickers : found on ebay, sold by \u0026ldquo;palma.fr2015\u0026rdquo; Adhesive black molding : found on ebay, sold by \u0026ldquo;procardetails\u0026rdquo; Black and white brillant spray paint, local DIY Shop Build steps Painting I first started to paint all visible parts. For the plexiglass used as bezel, as it was protected by a plastic film, I just removed the some plastic film on both sides before painting.\nAssembly Preparation Assembly preparation consisted in measuring, do some \u0026ldquo;pre-holes\u0026rdquo; and fixes HETTICH plastic assembly parts\nButtons \u0026ldquo;hack\u0026rdquo; Before mounting buttons on the control pannel, I had to modify a little bit every wire, because, by default, buttons were back-lit only when pressed and I wanted them to be always back-lit.\nBelow is the modification : left plug is the orignal, right plug is the modified one:\nControl pannel For the control pannel, the main important part was to center correctly joysticks and arrange screw holes to make same as less visible as possible.\nTo do so, I did some very small hole from the back, where I drew my measures. Then I did a bigger hole of the same size as skrew head on the top. Finaly, I did a hole of the same size of skrew body from the top.\nAssembly part 1 The first assembly part consisted in skrewing main parts, putting plastic foots on the bottom, cut and glue decorations, fix speakers and finaly mount the LCD pannel.\nFor the LCD panel, I first calibrated the position with its original foot, fixed the support and finaly removed the panel\u0026rsquo;s foot:\nPut all inside Before finalizing the assembly, I started to install some internal parts that would have been more difficult to mount later: motherboard, marquee and bottom leds, power supply, ssd, and external USB connectors.\nAssembly part 2 The final assembly part consisted in mounting the marquee, the backdoor, the power plug, the fan, the wii sensors and a small straight lug to make the control panel more aesthetic.\nRegarding the marquee, I put some aluminium adhesive tape all around the led strip in order to increase luminosity, and to have a better light diffusion behind the graphic and the plexiglass. I used corner and straight lugs to fix the marquee graphic, which is sandwitched beetween two sheets of plexiglass.\nFor the Backdoor, I fixed the power plug and the fan directly on it, then I attached 3 cupboard\u0026rsquo;s door hinges as well as two magnets on the upper side to maintain it closed.\nI also fixed the audio amplifier on the right side and made some holes to keep volume and tone buttons accessible when the backdoor is closed.\nFinaly, I fixed the dolphin bar just below the marquee, and I added a straight lug on the front egde of the control panel to make it looks better, more polished.\nFull view After I put the adhesive molding on edges, the finished product was there !\nSoftware Part For the software part, I used a tuned Windows 7, Hyperspin as front-end and the following emulators :\nDaphne MameUIFX 0.174 64 bit / nonag / direct input Nebula Supermodel Zinc Futur Pinball I also used Touchmote and JoyToKey to customize controls as well as some custom shell scripts to launch emulators.\nThe picture on the right show how I structured the data.\nTutorials and documentation sources There is a lots of information and tutorials on the internet for both optimizing Windows and configuring Hyperspin. Below are just links on sites that have inspired me:\nhttp://www.gamoover.net/Forums/index.php?topic=28411.0 http://www.techrepublic.com/blog/10-things/10-ways-to-speed-up-windows-7/ https://www.poweradmin.com/blog/how-to-optimize-and-speed-up-windows-7-performance/ http://www.disk-partition.com/kb/tips-ssd-optimization-windows7-1.html http://dsync.blogspot.fr/2015/05/extended-guide-on-setting-up-hyperspin.html To download videos for ingame previews, I used the chrome plugin \u0026ldquo;Flash Video Downloader\u0026rdquo; on www.newsvideo99.com and www.gamesdatabase.org\nControls Wiimote / gun Dolphin bar support multiple wiimotes, but this seems to only work correctly with the dolphin emulator. Other way of using it for mouse emulation (light gun), works only with one wiimote (afaik).\nHere is how I set it up for emulators:\nUse dolphin bar mode 4 : direct input detects it as a standard mouse Use Touchmote to simulate a second mouse button (mandatory for SuperModel) by assigning a keyboard key to button A (button B is the left mouse button by default) As Nebula emulator recognize both mouse buttons natively, I created a special emtpy Touchmote profil to disable it for Nebula (\u0026ldquo;emulator_multicpu.exe\u0026rdquo;):\nJoyToKey I used this soft mainly for 2 reasons :\nMap some non-configurable keys for \u0026ldquo;futur pinball\u0026rdquo; (eg: tilt) Configure some windows standard keys, like alt+f4 (quit emulators) or alt-tab (to have a workaround for focus problem that appears with futur pinball) Extract of my JoyToKey configuration (HeavyBox.cfg):\n... [Joystick 1] ... Button01=1, 09:00:00:00, 0.000, 0, 0 Button02=0 Button03=1, 51:00:00:00, 0.000, 0, 0 Button04=0 Button05=0 Button06=0 Button07=0 Button08=0 Button09=1, 12:00:00:00, 0.000, 0, 0] ... [Joystick 2] ... Button03=1, 51:00:00:00, 0.000, 0, 0 Button04=0 Button05=0 Button06=0 Button07=1, 20:00:00:00, 0.000, 0, 0 Button08=0 Button09=1, 73:00:00:00, 0.000, 0, 0 ... Emulators settings Before configuring Hyperspin, I first configured each emulator to optimize settings for best possible control and rendering without sacrifying fluidity.\nAs it depends on personnal preferences, I won\u0026rsquo;t details all controls unless I faced some specific issues.\nDaphne Default video options was ok, but setting up input was a bit tricky. I had to change a part from the GUI (mostly suppress some assigned keys) and assign joystick buttons directly in the config file.\n[KEYBOARD] KEY_UP = 273 264 0 KEY_LEFT = 276 260 0 KEY_DOWN = 274 258 0 KEY_RIGHT = 275 262 0 KEY_START1 = 49 0 9 KEY_START2 = 50 0 0 KEY_BUTTON1 = 32 306 2 KEY_BUTTON2 = 308 0 3 KEY_BUTTON3 = 304 0 6 KEY_COIN1 = 53 99 1 KEY_COIN2 = 54 0 0 KEY_SKILL1 = 267 0 4 KEY_SKILL2 = 268 0 5 KEY_SKILL3 = 269 0 7 KEY_SERVICE = 57 0 0 KEY_TEST = 283 0 0 KEY_RESET = 284 0 0 KEY_SCREENSHOT = 293 0 0 KEY_QUIT = 27 0 0 KEY_PAUSE = 112 0 0 KEY_TILT = 116 0 0 END Future pinball Despite the fact that some controls are not customizable (but JoyToKey helped here), input settings was easy with futur pinball.\nIt took me some time to find the best suited video settings to get the best out of my hardware:\nMame Here again, settings up Mame was not difficult, but having something that provides the best experience possible was tricky at some points.\nSo I made general settings for 80% of my games, then I fine tuned settings for some slow and/or vertical games.\nI also tried all possible settings to get no or very few tearing with my configuration.\nFor slightly slow but yet playable games (eg. Deathsml), the autoframeskip works correctly, but I had to change audio latency in game\u0026rsquo;s specific ini file (eg. deathsml.ini):\naudio_latency 4 Finaly, for vertical game, I downloaded bezel graphics when I found them and I used specific game\u0026rsquo;s ini file to (eg: dkong.ini):\nkeepaspect 1 unevenstretch 1 Nebula Nothing special here. I juste used the \u0026ldquo;multicpu\u0026rdquo; exe and set the resolution to 800x600 as there was no real visual enhencement to go with more pixels\u0026hellip;\nSuperModel This emulator run some Sega Model 3 games surprisingly well. Setting up input is quite easy, but all must be done in the config file. I just searched and set correct input everytime I saw \u0026ldquo;JOY1_XX\u0026rdquo; or \u0026ldquo;JOY2_XX\u0026rdquo;.\nNevertheless, I did not manage to make \u0026ldquo;off screen\u0026rdquo; gun detection with my WiiMote. That\u0026rsquo;s why I used Touchmote to assign a specific button (Key B) on the WiiMote\u0026rsquo;s A button and then configured consequently the emulator :\nInputOffscreen = \u0026#34;KEY_B,JOY2_BUTTON2,MOUSE_RIGHT_BUTTON\u0026#34; ; point off-screen InputAutoTrigger = 0 ; automatic reload when off-screen Zinc Zinc run perflectly smooth, with no problem on input. Below the rederer.cfg file I used. After multiple tests, using higher resolutions were not better looking together with filtering and blending.\n; ogl/d3d renderer settings XSize = 640 ; Window/fullscreen X size YSize = 480 ; Window/fullscreen Y size FullScreen = 1 ; Fullscreen mode: 0/1 ColorDepth = 32 ; Fullscreen color depth: 16/32 ScanLines = 0 ; Scanlines: 0=none, 1=black, 2=bright Filtering = 3 ; Texture filtering: 0-3 (filtering causes glitches!) Blending = 2 ; Enhanced color blend: ogl: 0/1; D3D: 0-2 Dithering = 0 ; Dithering: 0/1 (only needed in 16 bit color depths) ShowFPS = 0 ; FPS display on startup: 0/1 FrameLimitation = 1 ; Frame limit: 0/1 FrameSkipping = 0 ; Frame skip: 0/1 FramerateDetection = 1 ; Auto framerate detection: 0/1 FramerateManual = 60 ; Manual framerate: 0-1000 TextureType = 3 ; Textures: 0=card\u0026#39;s default, 1=4 bit, 2=5bit, 3=8bit TextureCaching = 2 ; Caching type: 0-3, def=2, mode 3 is not available on most cards EnableKeys = 1 ; Enable renderer keys: 0/1, def=1 (enables keys for the fps menu/pause) FastExcel = 0 ; Speed hack for SF \u0026#39;excel\u0026#39; modes. Will cause glitches if enabled! Hyperspin settings Hyperspin is a very nice front-end with lot of arcade style animations and beautiful themes for games. I used the version 1.4 which correct some issues with joystick control.\nHowever, like many others front-end, Hyperspin has been made to be organized on a per emulator basis\u0026hellip; Which is, IMHO, not very practical with lot of arcade games. I personally prefer a \u0026ldquo;game gender\u0026rdquo; organization (excepted for Daphne game, but don\u0026rsquo;t ask why, its just a matter of personnal taste !).\nSuch organization is possible with Hyperspin but it implies 2 things:\nMain menu and gender themes (wheel) must be created manually Game launcher must be a custom one, because all emulator games are mixed For the 2nd point, I created some scripts (see Custom scripts below). As for the first point, it was just a matter of time and some file editions.\nCustom main menu \u0026amp; games list The main menu is easy to create : a simple XML file in /Databases/Main Menu/Main Menu.xml, here is mine :\n\u0026lt;menu\u0026gt; \u0026lt;game name=\u0026#34;Beat em all\u0026#34;/\u0026gt; \u0026lt;game name=\u0026#34;Daphne\u0026#34;/\u0026gt; \u0026lt;game name=\u0026#34;Driving\u0026#34;/\u0026gt; \u0026lt;game name=\u0026#34;Fighting\u0026#34;/\u0026gt; \u0026lt;game name=\u0026#34;Gun\u0026#34;/\u0026gt; \u0026lt;game name=\u0026#34;Hack \u0026amp; Slash\u0026#34;/\u0026gt; \u0026lt;game name=\u0026#34;Pinball\u0026#34;/\u0026gt; \u0026lt;game name=\u0026#34;Platform\u0026#34;/\u0026gt; \u0026lt;game name=\u0026#34;Puzzle\u0026#34;/\u0026gt; \u0026lt;game name=\u0026#34;Shoot em up\u0026#34;/\u0026gt; \u0026lt;game name=\u0026#34;Sports\u0026#34;/\u0026gt; \u0026lt;/menu\u0026gt; Then, for each entry, a directory with the exact same name must be created both in /Databases and /Medias for respectively gender game list and game theme.\nI used Don\u0026rsquo;s HyperSpin Tools to build game list files, and then put each xml file in the correct gender\u0026rsquo;s directory. Eg for \u0026ldquo;beat em all\u0026rdquo; in my case : C:\\_HYPERSPIN\\Databases\\Beat em all\\Beat em all.xml\nGames \u0026amp; Gender themes Games themes has to be put directly in /Databases/. Eg, for \u0026ldquo;beat em all\u0026rdquo; in my case : C:\\_HYPERSPIN\\Media\\Beat em all\\Themes\nA lot of game themes can be found on Hyperspin website if you create an account.\nAs for gender themes, I had to build them myself based on:\nmame gender for background and wheel titles (/Media/MAME/Images/Genre) emulator themes for zip file structure and video preview container (eg. /Media/SNK Neo Geo/Themes/Default.zip Each zip file must be named like the gender in the /Media/Main Menu/Themes. Eg. for \u0026ldquo;beat em all\u0026rdquo; in my case: Beat em all.zip\nCustom scripts Main launcher As explained previously, mixing games from multiple emulators required to build a custom script to launch games.\nThe purpose of this script is to determine the emulator to execute depending on the path where the rom file is (because on the filesystem I organized rom files on a per emulator basis, then gender).\n@echo off for /f \u0026#34;delims=\u0026#34; %%A in (\u0026#39;dir C:\\_ROMS\\ /s /b ^| find /I %1\u0026#39;) do set \u0026#34;fullpath=%%A\u0026#34; echo \u0026#34;full path is: %fullpath%\u0026#34; for %%f in (%1) do set game=%%~nf echo \u0026#34;game\u0026#39;s name: %game%\u0026#34; REM does not work inside zinc condition... so I put it there for /F %%B in (\u0026#39;find \u0026#34;%game%\u0026#34; C:\\_EMU\\zinc11-win32\\gamelist.txt\u0026#39;) do set \u0026#34;number=%%B\u0026#34; echo %fullpath% | find /I \u0026#34;Mame\u0026#34; \u0026gt; nul \u0026amp;\u0026amp; ( cd C:\\_EMU\\mameuifx64 echo \u0026#34;starting mame: mameuifx64-175.exe %game%\u0026#34; mameuifx64-175.exe %game% ) echo %fullpath% | find /I \u0026#34;Model2\u0026#34; \u0026gt; nul \u0026amp;\u0026amp; ( cd C:\\_EMU\\m2emulator echo \u0026#34;starting m2 emu: emulator_multicpu.exe %game%\u0026#34; emulator_multicpu.exe %game% ) echo %fullpath% | find /I \u0026#34;SuperModel\u0026#34; \u0026gt; nul \u0026amp;\u0026amp; ( cd C:\\_EMU\\Supermodel_0.2a_Win64 echo \u0026#34;starting super model emu: start_super.bat \u0026#34;%fullpath%\u0026#34;\u0026#34; start_super.bat \u0026#34;%fullpath%\u0026#34; ) echo %fullpath% | find /I \u0026#34;Zinc\u0026#34; \u0026gt; nul \u0026amp;\u0026amp; ( cd C:\\_EMU\\zinc11-win32 echo \u0026#34;Zinc game number found: %number%\u0026#34; start_zinc.bat %number% ) cd C:\\_HYPERSPIN cscript.exe focus.vbs \u0026#34;HyperSpin\u0026#34; As you can see, I used other custom scripts to launch SuperModel and Zinc emulator as well as a special \u0026ldquo;focus.vbs\u0026rdquo; script to launch Hyperspin.\nThis \u0026ldquo;focus.vbs\u0026rdquo; script allows to force the focus to a specific task giving its name. This helps to bypass some lost of focus bugs with hyperspin during startup and when an emulator exits.\nif WScript.Arguments.Count = 0 then WScript.Echo \u0026#34;windows name to activate\u0026#34; end if Dim shl Set shl = CreateObject(\u0026#34;WScript.Shell\u0026#34;) shl.AppActivate WScript.Arguments(0) Here is how I configured HyperSpin to launch games :\nSpecific emulator launcher Excepted for Mame and Nebula which get all their parameters from config files, I had to create specific scripts to launch other emulators.\nDaphne:\nfor %%f in (%1) do set game=%%~nf echo \u0026#34;game\u0026#39;s name: %game%\u0026#34; daphne.exe %game% vldp -framefile C:\\_EMU\\Daphne\\vldp\\%game%\\%game%.txt -fullscreen -noserversend -opengl Supermodel:\nSupermodel.exe -res=800,600 -fullscreen %1 Zinc:\nzinc.exe %1 --roms-directory=C:\\_ROMS\\Zinc --use-sound=yes --sound-filter-enable=yes --sound-filter-cutoff=44100 --sound-surround-lite-enable=yes --sound-surround-lite-multiplier=40 --sound-stereo-exciter=yes --use-slow-geometry=yes --use-stackinram-hack=no --use-mem-predict=no Future pinball launcher As futur pinball do not use \u0026ldquo;.zip\u0026rdquo; I made a specific launcher script and a dedicated Hyperspin config:\nThe script :\n@echo off CD \u0026#34;C:\\_EMU\\Future Pinball\u0026#34; \u0026#34;future pinball.exe\u0026#34; /open %1 /play /exit cd C:\\_HYPERSPIN cscript.exe focus.vbs \u0026#34;HyperSpin\u0026#34; Unfortunately, I still have focus problem with Future pinball : it is launched correctly, but controls does not work even when I try to force focus on it. The only way I found is to use \u0026ldquo;CTRL+TAB\u0026rdquo;. So again, I used JoyToKey to map CTRL and TAB key to two differents buttons.\nStarting HyperSpin To launch Hyperspin instead of the standard Windows shell I modified the registry key \u0026ldquo;HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\u0026rdquo; with the following :\nC:\\Windows\\System32\\cmd.exe /C /Q C:\\_HYPERSPIN\\hyperspin_startup.bat Yet another custom script ! It allows to start everything (Hyperspin, JoyToKey, touchmote) without focus problem nor lagging interface:\n@echo off start C:\\_HYPERSPIN\\Joy2key\\JoyToKey.exe timeout 10 cscript.exe focus.vbs \u0026#34;HyperSpin\u0026#34; Final result ","permalink":"https://www.bluemind.org/homemade-bartop-retrogaming/","summary":"\u003cp\u003eThe story began some years ago, when I built from scratch a \u0026ldquo;true\u0026rdquo; fullsized arcade cabinet (\u003ca href=\"/misc-galleries/nggallery/misc-galleries/mame-cab\"\u003esee the gallery\u003c/a\u003e). This Mame Cab was pretty cool, with nice arcade feeling : CRT screen, optical guns (ps 1), trackball, real coin door.\u003c/p\u003e\n\u003cp\u003eThe problem was the size of the beast : HxLxD was 190x80x120 cm. It was taking a lot of space\u0026hellip; Space that I needed to create a new study area.\u003c/p\u003e","title":"Retrogaming: homemade bartop"},{"content":"As part of a project to upgrade my servers (Sheevaplug and Cubieboards), I wanted to automate most of the OS and software deployments.\nMoreover the idea was also to factorize configurations for all servers and be able to change and replay deployments easily\u0026hellip; That\u0026rsquo;s exactly the purpose of an orchestrator like Puppets, Ansible or Saltstack.\nI choosed Saltstack as it seemed the most simple to use to me without any compromise on functionalities (regarding my needs).\nBelow are my notes about this first install, as well as a little script to to even automate the minions deployment.\nBase install Both Archlinux and Saltstack projects provide good documentation to start: https://wiki.archlinux.org/index.php/Saltstack https://docs.saltstack.com/en/latest/ref/configuration/index.html\nUnfortunately the \u0026ldquo;raet\u0026rdquo; package was not working correctly on arm arch when I tried: it was taking 100% of cpu. So I installed both on Master and Minion the \u0026ldquo;zmq\u0026rdquo; version:\npacman -S salt-zmq Next step : create a dns entry for the master, eg: \u0026ldquo;salt.local.lan\u0026rdquo; , so it is easier to change the host without impact on minion\u0026rsquo;s config file.\nI also did the same for all minions, eg: \u0026ldquo;minion.local.lan\u0026rdquo;.\nSaltstack use 2 TCP ports that must be opened for master and minon if they are on different subnets or vlan protected by a firewall.\nExample for an iptables rules file :\n-A INPUT -p tcp -i vlan1 --dport 4505:4506 -j ACCEPT -A INPUT -p tcp -i vlan2 --dport 4505:4506 -j ACCEPT Enable communication between master and minion To make the master deals with the minion, it is needed to :\nTell the minion who is it\u0026rsquo;s master (darth Sidious ?) Make the master trusts the minion Setting the minion\u0026rsquo;s master requires to set the \u0026ldquo;master\u0026rdquo; property in \u0026ldquo;/etc/salt/minion\u0026rdquo;:\nmaster: salt.local.lan Then the minion must have the master\u0026rsquo;s public key fingerprint in \u0026ldquo;/etc/etc/salt/minion\u0026rdquo; (the property is named \u0026ldquo;master_finger\u0026rdquo;). To get the master key (Saltstack must be running) :\nsudo systemctl start salt-master salt-key -F master As soon as the key is set in the minion\u0026rsquo;s config file, saltstack can be started on the minion:\nsudo systemctl start salt-minion The last step to establish the communication is to allow the minion on the master side:\nsudo salt-key -a minion.local.lan then test with a \u0026ldquo;ping\u0026rdquo; :\nsudo salt \u0026#39;*\u0026#39; test.ping Final Adjustments Everything works, so we can start Saltstack at boot time, on the master:\nsudo systemctl enable salt-master And on the minion:\nsudo systemctl enable salt-minion Configure log rotation properly (logrotate) with the following in a new file /etc/logrotate.d/salt\n/var/log/salt/key /var/log/salt/master /var/log/salt/minion { nocompress missingok postrotate /bin/kill -HUP `cat /var/run/salt/salt-master.pid 2\u0026gt;/dev/null` 2\u0026gt; /dev/null || true endscript } As some of my minions as well as my master run on low power arm computer (eg: raspberry or odroid c1), I changed the master configuration in /etc/salt/master to set the \u0026ldquo;timeout\u0026rdquo; property to 120 and the \u0026ldquo;worker_thread\u0026rdquo; to 8.\nThis allows to be more tolerant for slow answer as the master can wait up to 2 minutes for 8 command\u0026rsquo;s results.\nAt this time, salt is able to drive minions\u0026hellip; but the true power of SaltStack is the templating of services: the states\u0026hellip;\nConfigure States Prepare folder architecture States are configuration templates that are OS agnostic. Saltstack propose a directory and filename based logic to handle the whole thing.\nThere are basicaly 3 main directories to configure in \u0026ldquo;/etc/salt/master\u0026rdquo;:\nfile_roots : for states path module_dirs: for modules (to create custom state\u0026rsquo;s actions) pillar_roots: pillars (Salstack way of defining and assigning values to minions) Example of structure :\n/srv/salt/states/base /srv/salt/pillar/base /srv/salt/modules A simple state As an example, here is a state that remove \u0026ldquo;alarm\u0026rdquo; user and group on am Archlinuxarm distrib.\nCreate the folder \u0026ldquo;noalarmuser\u0026rdquo; in /srv/salt/states/base, then a file named \u0026ldquo;init.sls\u0026rdquo; with the following content:\nalarm: user.absent: - purge: True remove_alarm_group: group.absent: - name: alarm To apply this state to a minion, it is as simple as :\nsalt \u0026#39;minion.local.lan\u0026#39; state.apply noalarmuser There are 4 kind of information to build a state :\nThe state name, here defined as a the folder name \u0026ldquo;noalarmuser\u0026rdquo; which contains a default definition file \u0026ldquo;init.sls\u0026rdquo; State actions identifiers : \u0026ldquo;alarm\u0026rdquo; and \u0026ldquo;remove_alarm_group\u0026rdquo;. They have to be unique. Modules and functions to execute: \u0026ldquo;user.absent\u0026rdquo; and \u0026ldquo;group.absent\u0026rdquo; Function\u0026rsquo;s parameters : \u0026ldquo;purge: True\u0026rdquo; and \u0026ldquo;name: alarm\u0026rdquo; A majority (if not all ?) module\u0026rsquo;s function use a first parameter \u0026ldquo;name\u0026rdquo; which default value is taken from the action identifier.\nFor example, the first state action could have defined as :\nany_unique_action_identifier: user.absent: - name: alarm - purge: True Assign states to minons Saltstack allows to apply a state to one or several minions, but also allows to define which state should be applied to which minion.\nAssignation of states to minion is done by defining a file name \u0026ldquo;top.sls\u0026rdquo; in the \u0026ldquo;base\u0026rdquo; directory. Example in \u0026ldquo;/srv/salt/states/base/top.sls\u0026rdquo;:\nbase: \u0026#39;*.local.lan\u0026#39;: - noalarmuser This allows to remove alarm user and groups to all minions which name ends with \u0026ldquo;.local.lan\u0026rdquo;.\nTo apply states that are set in the top.sls file to a specific minion:\nsalt \u0026#39;minion.local.lan\u0026#39; state.apply Script to automate Salstack deployment on minions I\u0026rsquo;m now using the following script (which is also attached to this article) to install and configure saltstack on any new Archlinux based machine to transform it into a minion.\nTo use it, just replace \u0026ldquo;xx:xx:xx:xx:xx:xx:xx:xx\u0026rdquo; by your master.pub key finger print:\n#!/bin/bash if [[ $EUID -ne 0 ]]; then echo \u0026#34;This script must be run as root\u0026#34; exit 1 fi # install salt and set master in config file pacman --noconfirm -Sy salt-zmq sed -i -e\u0026#34;s/^#master\\s*:\\s*salt/master: salt.local.lan/\u0026#34; /etc/salt/minion sed -i -e\u0026#34;s/^#master_finger\\s*:\\s*\\\u0026#39;\\\u0026#39;/master_finger: \\\u0026#39;\u0026#39;xx:xx:xx:xx:xx:xx:xx\u0026#39;\\\u0026#39;/\u0026#34; /etc/salt/minion # add local domain to minion_id echo -n \u0026#34;$HOSTNAME.local.lan\u0026#34; \u0026gt;\u0026gt; /etc/salt/minion_id # add logrotate config cat \u0026lt;\u0026lt;EOL \u0026gt;\u0026gt; /etc/logrotate.d/salt /var/log/salt/key /var/log/salt/master /var/log/salt/minion { nocompress missingok postrotate /bin/kill -HUP \\`cat /var/run/salt/salt-minion.pid 2\u0026gt;/dev/null\\` 2\u0026gt; /dev/null || true endscript } EOL systemctl restart logrotate # show minion id MINION_ID=`cat /etc/salt/minion_id` echo \u0026#34;\u0026#34; echo \u0026#34;minion_id set to : $MINION_ID\u0026#34; echo \u0026#34; =\u0026gt; check that this hostname is declared in local dns\u0026#34; echo \u0026#34;\u0026#34; # enable \u0026amp; start service systemctl enable salt-minion systemctl start salt-minion sleep 2 # final word echo \u0026#34;\u0026#34; ps ax | grep salt echo \u0026#34;\u0026#34; echo \u0026#34;\u0026#34; echo \u0026#34;type the following on the master to authorize this minion:\u0026#34; echo \u0026#34;salt-key -a $MINION_ID\u0026#34; Download the script : salt-minion-install\n","permalink":"https://www.bluemind.org/deploying-saltstack-master-minion-archlinux-arm/","summary":"\u003cp\u003eAs part of a project to upgrade my servers (\u003ca href=\"/linux-sheevaplug-perfect-nas-reloaded/\"\u003eSheevaplug\u003c/a\u003e and \u003ca href=\"/hardware-cookie-box-host-3-cubieboards/\"\u003eCubieboards\u003c/a\u003e), I wanted to automate most of the OS and software deployments.\u003c/p\u003e\n\u003cp\u003eMoreover the idea was also to factorize configurations for all servers and be able to change and replay deployments easily\u0026hellip; That\u0026rsquo;s exactly the purpose of an orchestrator like Puppets, Ansible or Saltstack.\u003c/p\u003e\n\u003cp\u003eI choosed Saltstack as it seemed the most simple to use to me without any compromise on functionalities (regarding my needs).\u003c/p\u003e","title":"Deploying Saltstack : master and minion (archlinux on ARM)"},{"content":"Following my original project of the Zibase Binding, I updated it for the recent 1.9 branch.\nThe pull request has been sent to OpenHab team. While this update is being reviewed, you can find the binary binding below.\nCompared to the first published version, the changes are :\nBug corrections:\nhandle case where the Ziibase does not respond (j-zapi return null) handle case where jzapi return null for receiver value (was craching the whole thread) Updates / additions:\nadded support for Chacon emitter (id like H8, I12, etc.) added debug log entry when item ID is null changed jzapi 0.0.5 by 0.0.6 Download : org.openhab.binding.zibase-1.9.0-SNAPSHOT\n","permalink":"https://www.bluemind.org/project-update-openhab-binding-zibase/","summary":"\u003cp\u003eFollowing \u003ca href=\"/project-openhab-binding-zibase/\"\u003emy original project of the Zibase Binding\u003c/a\u003e, I updated it for the recent 1.9 branch.\u003c/p\u003e\n\u003cp\u003eThe \u003ca href=\"https://github.com/openhab/openhab/pull/4116\"\u003epull request\u003c/a\u003e has been sent to OpenHab team. While this update is being reviewed, you can find the binary binding below.\u003c/p\u003e\n\u003cp\u003eCompared to the first published version, the changes are :\u003c/p\u003e\n\u003cp\u003eBug corrections:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ehandle case where the Ziibase does not respond (j-zapi return null)\u003c/li\u003e\n\u003cli\u003ehandle case where jzapi return null for receiver value (was craching the whole thread)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eUpdates / additions:\u003c/p\u003e","title":"Project : update of OpenHab binding for Zibase"},{"content":"Since more than 2 years now, I\u0026rsquo;m using a Sheevaplug as a low power NAS (see this article). Until now, I was using a Debian 6 \u0026ldquo;squeeze\u0026rdquo;, with an old 2.6 kernel.\nI had some trouble with Samba, like crash with high usage on small files (eg. Kodi\u0026rsquo;s library update). In addition, after 2 years of Debian, I was still not convinced and I wanted to use my favorite distro : Archlinux.\nI\u0026rsquo;m using the same \u0026ldquo;NAS softwares\u0026rdquo;, but a little bit more tuned\u0026hellip;\nBase install At the time I dit the whole install (October, 12), following the official Archlinux Arm install doc was not working.\nSDCard install On a SDCard, I created a small FAT partition (128 Mb), and another ext4 partition with the remaining space. As always, I optimized the SDCard (see this article). and formated the ext4 partition with (SDCard as /dev/sdb):\nmkfs.ext4 -O \u0026#39;^has_journal\u0026#39; -E stride=2,stripe-width=1024 -b 2048 /dev/sdb2 Then I deployed Archlinux base system on the SDCard (from my Laptop):\ncd /tmp mkdir root mkdir root/boot mount /dev/sdb2 root mount /dev/sdb1 root/boot wget http://archlinuxarm.org/os/ArchLinuxARM-kirkwood-latest.tar.gz bsdtar -xpf /home/juju/Downloads/ArchLinuxARM-kirkwood-latest.tar.gz -C root sync umount root/boot umount root After that, booting required to use the serial connection (usb cable) to set some uboot variables. I plugged the cable to my laptop and switched on the Sheevaplug. Then I used GNU Screen to connect to the Sheevaplug. I had to do this very quickly in order to interrupt the boot process by pressing as soon as the Sheevaplug booted:\nscreen 115200 8n1 setenv bootcmd \u0026#39;setenv bootargs $(bootargs_console) root=/dev/mmcblk0p2 rootdelay=5; run bootcmd_mmc; bootm 0x00800000\u0026#39; setenv bootcmd_mmc \u0026#39;mmc init; mmc init; ext2load mmc 0:1 0x00800000 /uImage;\u0026#39; saveenv boot System configuration Setup the base system First actions I did :\nchange the hostname (/etc/hostname) create a new user delete the default \u0026ldquo;alarm\u0026rdquo; user (userdel alarm then rm -R /home/alarm) change root password change /etc/systemd/journald.conf to lesser SDCard write : SyncIntervalSec=10m custom mount options in /etc/fstab for root filesystem : /dev/mmcblk0p2 / ext4 defaults,async,barrier=0,commit=100,noatime,nodiratime,errors=remount-ro 0 1 Install Yaourt and prepare for AUR I set optimized CCFLAGS and CXXFLAGS in /etc/makepkg.conf and added the following parameters to the default ones (see https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html and https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html) :\n-mtune=xscale -fweb -frename-registers -fomit-frame-pointer Then, I installed yaourt by\ninstalling base-devel : pacman -S base-devel adding my user to /etc/suders (yaourt refuse to run as root, for safety reasons) adding archlinuxfr repo to /etc/pacman.conf : Server = http://repo.archlinux.fr/arm At this point, I had to manualy install package-query for arm as yaourt depends on it, but package-query does not exists as compiled package for arm devices\ngit clone https://aur.archlinux.org/package-query.git cd package-query makepkg -Acs sudo pacman -U package-query-1.7-1-arm.pkg.tar.xz sudo pacman -S yaourt System tools Cronie for cron (https://wiki.archlinux.org/index.php/Cron#Cronie) :\nyaourt -S cronie timedatectl set-timezone Europe/Paris I changed /etc/anacrontab to make cronjobs execution during the night :\nSTART_HOURS_RANGE=2-7 Then I installed hdparm and a systemd rc.local implementation to easily put some customisations at boot.\nyaourt -S hdparm rc.local sudo systemctl enable rc-local /etc/rc.local with customization :\n# deadlines cheduler could group small accesses to lesser sdcard latency echo deadline \u0026gt; /sys/block/mmcblk0/queue/scheduler echo deadline \u0026gt; /sys/block/sda/queue/scheduler echo deadline \u0026gt; /sys/block/sdb/queue/scheduler # NAS Disks setting # set hd standby # 50 sec for mirror disk hdparm -S 10 /dev/sdb \u0026amp; # 24 min for data disk hdparm -S 255 /dev/sda \u0026amp; # optimize disk readahead buffer / noise option /usr/bin/hdparm -a 1024 /dev/sda /usr/bin/hdparm -a 1024 /dev/sdb /usr/bin/hdparm -M 254 /dev/sda /usr/bin/hdparm -M 254 /dev/sdb # eth0 optimize /usr/bin/ifconfig eth0 txqueuelen 5000 Finaly :\nAllow the server to send mails : https://wiki.archlinux.org/index.php/SSMTP Monitor NAS drive by installing smartmontools and follow instructions at https://wiki.archlinux.org/index.php/S.M.A.R.T. NAS Softwares Samba and network optimizations As always the Archlinux wiki was a very good start point. The important steps are :\nsetting \u0026ldquo;hosts allow\u0026rdquo; to restrict accesses set \u0026ldquo;encrypt passwords = yes\u0026rdquo; set \u0026ldquo;security = user\u0026rdquo; create users according to systems accounts with \u0026ldquo;smbpasswd -a \u0026rdquo; I found 2 great articles about network optimizations: https://linuxengineering.wordpress.com/2014/08/03/performance-tuning-with-pogoplug-v4 (similar to Sheevaplug, but a little less powerfull) http://datatag.web.cern.ch/datatag/howto/tcp.html\nI ended up with the following customizations with which I currently have 18.6 Mb/s when reading and 17.8 Mb/s when writting.\n/etc/sysctl.d/10-iptuning.conf :\nnet.core.rmem_max = 5603328 # 0,75 of wmem_max rounded to 4096 net.core.wmem_max = 4194304 # set tcp mem to 4M max (default = 16k) net.ipv4.tcp_rmem = 4096 87380 5603328 net.ipv4.tcp_wmem = 4096 16384 4194304 net.ipv4.tcp_timestamps = 0 # less CPU usage on small arm soc net.core.optmem_max = 65535 net.core.netdev_max_backlog = 5000 /etc/samba/smb.conf (network settings only) :\nstrict allocate = Yes read raw = yes write raw = yes strict locking = No socket options = TCP_NODELAY SO_KEEPALIVE IPTOS_LOWDELAY SO_RCVBUF=131072 SO_SNDBUF=131072 min receivefile size = 4096 use sendfile = true aio read size = 4096 aio write size = 4096 oplocks = yes max xmit = 65535 max connections = 16 deadtime = 15 getwd cache = yes Regain : a search engine Regain is a nice and simple search engine, initialy made for desktop search. It is composed of a crawler and a web interface for searching\u0026hellip; So it can also be used as a server search engine.\nIt requires java (6 or 7) and tomcat6 to run. Installation guide can be found in Regain manual. I installed it in /opt/regain and made symlink for the webapp to be found by tomcat (in /var/lib/tomcat6/webapps)\nI also installed pdfbox to better index pdf files. I had to change the source url in the PKGBUILD (yaourt -S pdfbox) because the default site was too slow (see http://www.apache.org/dyn/closer.cgi for mirrors)\nI activated the use of PDFBox by uncommenting the dedicated \u0026ldquo;preparator\u0026rdquo; in /var/lib/tomcat6/conf/regain/SearchConfiguration.xml.\nWith tomcat6 started at this point, regain just displayed a message saying that there was no index. Building one needed to launch the indexer :\ncd /opt/regain/runtime/crawler java -Xms128m -Xmx128m -jar regain-crawler.jar It can take a lot of time depending on the number of files to index\u0026hellip;\nWith an index, the web interface allowed to search and see some \u0026ldquo;google like\u0026rdquo; results. But clicking on a result did nothing !\u0026hellip; Regain is a desktop search engine, so it generates links like \u0026ldquo;file:///\u0026rdquo; which simply can\u0026rsquo;t work from a workstation.\nChance is, regain allows to change links prefixes. With Windows workstations, links like \u0026ldquo;\\\\server\\share\u0026rdquo; should work, but I only have Linux and Mac OS X clients. So the only \u0026ldquo;universal\u0026rdquo; prefix that could be recognized by any browser was\u0026hellip; the (not so) good plain old ftp protocol (ftp://server/). This protocol is clearly not secure and FTPS or SFTP don\u0026rsquo;t work with all browsers. So I choosed to use a simple FTP connection for search result consultations. It\u0026rsquo;s not secure, but I only use the search from time to time, so un-encrypted passwords are only passing my network very few times.\nMain options of my vsftpd :\nlocal users only limit to true users set home of all users to /home (for chroot) CHROOT users Then in /var/lib/tomcat6/conf/regain/SearchConfiguration.xml, I added the following rewrite rules for all results :\n\u0026lt;rewriteRules\u0026gt; \u0026lt;rule prefix=\u0026#34;file:///myShares\u0026#34; replacement=\u0026#34;ftp://myServer.address\u0026#34;/\u0026gt; \u0026lt;/rewriteRules\u0026gt; I could have used tomcat to securely access files in https by activating directory listing, but it would have meant\nto give tomcat user right to access all files - not that secure to duplicate system accounts for access restriction - not really nice to chroot tomcat user to where NAS sources are - not sure it could work correctly to set a signed certificate - hmm\u0026hellip; that\u0026rsquo;s just a home network ;) ","permalink":"https://www.bluemind.org/linux-sheevaplug-perfect-nas-reloaded/","summary":"\u003cp\u003eSince more than 2 years now, I\u0026rsquo;m using a \u003ca href=\"https://en.wikipedia.org/wiki/SheevaPlug\"\u003eSheevaplug\u003c/a\u003e as a low power NAS (see \u003ca href=\"/linux-sheevaplug-perfect-nas/\"\u003ethis article\u003c/a\u003e). Until now, I was using a \u003ca href=\"https://www.debian.org/releases/squeeze/\"\u003eDebian 6 \u0026ldquo;squeeze\u0026rdquo;\u003c/a\u003e, with an old 2.6 kernel.\u003c/p\u003e\n\u003cp\u003eI had some trouble with Samba, like crash with high usage on small files (eg. Kodi\u0026rsquo;s library update). In addition, after 2 years of Debian, I was still not convinced and I wanted to use my favorite distro : \u003ca href=\"https://www.archlinux.org/\"\u003eArchlinux\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;m using the same \u0026ldquo;NAS softwares\u0026rdquo;, but a little bit more tuned\u0026hellip;\u003c/p\u003e","title":"Linux : sheevaplug as a perfect NAS - Reloaded"},{"content":"I have seen some guys who put a raspberry pi in a Game Boy case (eg. here or here). Being a fan of retro-gaming, I wanted to do the same for me and my children. But I wanted something a little bit more polished than what I saw, I mean better looking, better battery life, etc. I discovered at the same time the Odroid-W (which is, unfortunately, no more produced). The following lines are about the build story\u0026hellip;\nFeatures The Final result is a Game Boy lookalike device that runs various 8 and 16 bits systems:\nNintendo NES Nintendo Super NES Nintendo Game Boy / Game Boy Color Nintendo Game Boy Advance Sega Master System Sega Genesis Sega CD Nec Pc Engine / SuperGraphX Nec Pc Engine CD-Rom SNK NeoGeo Atari Link PC Doom 1 \u0026amp; Doom 2 (with original background music !) Main features of my \u0026ldquo;Retro Boy\u0026rdquo; are:\n3.5\u0026quot; TFT Color screen (NTSC, 60 Hz) 3 Usb 2.0 Ports Integrated Wifi 4 joypad buttons (a, b, L, R) + start / select Mono Speaker + Jack audio out (pluging headset cut the speaker output) Tv-Out (Composite) 4 - 5 hours of battery life Hardware Used components Odroid W (which was sold with a USB Connector (F) and pin headers Game Boy replacement case with silicon buttons + buttons PCB (I took it here) Small USB hub with flat cable (USB / Micro Usb) Raspberry pi copper heatsink Mini Speaker from Adafruit (ref. ada-1898) Class D mono audio amplifier from Adafruit (ref. ada-2130 A 3.5 Inches PAL/NTSC small TV Display from Adafruit (ref. ada-913) Push buttons from amazon (as found here) Some generic push buttons (for Wifi On/off, volume and power switch) Generic usb wifi dongle (linux compatible) 6bit Multicolor LED breadboard, found on ebay 2 x 2000 mAh lipo batteries from Sparkfun (ref. 8483) a Game boy (color) game (the cheapest I could found) Micro usb breakout board (charging connector) Off course, I also used some wires and electrical tape.\nGPIO Wiring plan Assembly steps USB Hub preparation I Unsoldered 2 ports : one for internal wifi and one for the right side, in place of the original Gameboy Link port. I also splitted the original USB hub cable to plug it directly on Odroid-W GPIO headers.\nDisplay modifications The display was said to work with 6-12V, but could easily be used with 5V by bypassing the voltage regulator (the display is orginaly for cars).\nI also replaced one of the two composite connectors by a jumper cable and added a small on/off switch. Turning the screen off allows for better battery life while pausing game and is also mandatory to use the composite output (else the video signal would be very poor and the video circuit could be damaged)\nPCB Soldering I soldered wires on buttons PCB and pin header on Odroid-W board and mounted the heatsink\nGame cartridge adaptation The purpose was to use an (part of) original game cartdrige to fill the game hole on the Game Boy case. I used it to add two buttons (wifi on / wifi off) as well as the power/charging micro Usb connector.\nCase modifications I Modified the Game Boy case with a Dremel in several ways : make room for batteries, holes for L/R buttons, switches mount, holes for leds (battery meter), various internal plastic removals, make the screen window bigger, colorize screen area borders in black, etc\u0026hellip;\nMounting all together I finaly wired and soldered all parts together (audio amplifier, speaker, batteries, leds, etc.)\nSoftware After all was mounted, I firstly installed a standard Raspbian image in order to test everything. But even with optimizations (disabling services, etc.), the booting delay was way to long for me. As an Archlinux fan, I replaced Rasbpian with Archlinux Arm\u0026hellip;\nBase Archlinux install on SDCard The only way to install things on a bare Archlinux is to use Pacman (or Yaourt) and an internet connection. Unfortunately, wpa-supplicant is not installed by default and my Retro Boy has only a Wifi card. So the first step was to use chroot and Qemu + BinFmt to install and configure wpa-supplicant before booting on the real hardware.\nI installed qemu on my x86 laptop and activated support for Arm binary execution (through qemu):\nyaourt -Sy qemu-user-static binfmt-support sudo echo \u0026#39;:arm:M::\\x7fELF\\x01\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x28\\x00:\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xfe\\xff\\xff\\xff:/usr/bin/qemu-arm-static:\u0026#39; \u0026gt; /proc/sys/fs/binfmt_misc/register Then, I chrooted to the sdcard (sdb2) as root:\nmount /dev/sdb2 /mnt/root cd /mnt/root mount -t proc proc proc/ mount --rbind /sys sys/ mount --rbind /dev dev/ mount --rbind /run run/ mount /dev/sdb1 boot mkdir run/systemd/resolve cp /etc/resolv.conf run/systemd/resolve/resolv.conf chroot /mnt/root /bin/bash Being finally chrooted on a Arm based Archlinux, I could do some operations as if I were on the native Odroid-w : updating the whole system and installing wpa-supplicant\npacman -Syu pacman -S wpa_supplicant Finaly, I created a special module configuration file in /etc/etc/modprobe.d/ for the wifi driver (8192cu) to make it stable. Without that, the Wifi connection dropped frequently (the conf file is available on the download section at the end of this article)\noptions 8192cu rtw_power_mgnt=0 rtw_enusbss=1 rtw_ips_mode=1 It was then ready to boot on the real hardware to activate Wifi and start configuring everything\u0026hellip;\nWifi Activation I configured wpa_supplicant in /etc/wpa_supplicant conf (more details on archlinux wiki), then :\nip link set wlan0 up wpa_supplicant -c /etc/wpa_supplicant/wpa_supplicant.conf -Dwext -B -i wlan0 dhcpcd wlan0 In order to turn on or off wifi connection with the dedicated buttons (see usage of esekeyd below), I also created two small bash scripts: one to start and one to stop wifi connection\n#!/bin/bash # Start Wifi connection modprobe 8192cu ip link set wlan0 up wpa_supplicant -Dwext -B -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf dhcpcd wlan0 #!/bin/bash # Stop wifi connection killall dhcpcd killall wpa_supplicant ip link set wlan0 down rmmod 8192cu Finaly, I disabled uneeded network related services :\nsystemctl disable dhcpcd.service systemctl disable netctl.service systemctl disable wicd.service systemctl enable NetworkManager Preparing for custom compilation of AUR packages The first two step I did was to install Yaourt (https://archlinux.fr/yaourt) and configuring a cross-compilation environment (compiling only on the broadcom SoC is just an huge time consuming task). See http://archlinuxarm.org/developers/distcc-cross-compiling.\nThen I created a dedicated user for makepkg (it does not run as root for obvious security reasons). I called it \u0026ldquo;pi\u0026rdquo; and added it in sudoers (/etc/sudoer, \u0026ldquo;pi ALL=(ALL) ALL\u0026rdquo;)\nBefore compiling, two more steps was required : tuning CCFLAGS for best possible performances and activating swap because 512 Mb is not always enought even when cross-compiling :\nI used the following make flags in /etc/makepkg.conf (see GCC doc)\n-march=armv6zk -mcpu=arm1176jzf-s -Ofast -fno-fast-math -mfloat-abi=hard -mfpu=vfp -pipe -fomit-frame-pointer -fstack-protector --param=ssp-buffer-size=4 Activation of swap and avoid /tmp to be mounted as tmpfs (I rebooted at this point):\nsystemctl mask tmp.mount dd if=/dev/zero of=/swapfile0 bs=1024 count=524288 mkswap /swapfile0 swapon /swapfile0 Installing and configuring emulators (retroarch) I only installed emulators that run at fullspeed with my overclock settings and EmulationStation as frontend\nyaourt -S retroarch-git-rpi yaourt -S libretro-gambatte-git libretro-handy-git libretro-gpsp-git libretro-fba-neogeo-git libretro-picodrive-git libretro-prboom-git libretro-fceumm-git libretro-pocketsnes-git libretro-mednafen-pce-fast-git yaourt -S emulationstation-git-unstable-rpi emulationstation-themes emulationstation-scraper-git The configurations files for Retroarch and EmulationStation are available in the download section at the end of this article.\nParticular retroarch parameters I tuned (mostly for speed) was :\nhistory_list_enable = \u0026#34;false\u0026#34; config_save_on_exit = \u0026#34;false\u0026#34; video_threaded = \u0026#34;false\u0026#34; audio_driver = \u0026#34;alsa\u0026#34; rewind_enable = \u0026#34;false\u0026#34; audio_latency = \u0026#34;256\u0026#34; video_refresh_rate = \u0026#34;59.940000\u0026#34; custom_viewport_width = \u0026#34;720\u0026#34; custom_viewport_height = \u0026#34;480\u0026#34; Note : EmulationStation\u0026rsquo;s additional themes can be found at http://blog.nilsbyte.de/downloads/\nOne particular thing for prboom : it needs to find prboom.wad file in the same directory as where original Doom 1 or 2 Wad files are stored (a simple \u0026ldquo;ln -s /usr/share/libretro/libretro-prboom/prboom.wad\u0026rdquo; is ok)\nNot working / slow tested engines During my tests, I did not retain 3 engines that I originaly wanted.\npcsx-rearmed in order to avoid dynamic linkage error, I changed the build() and package() function in PKGBUILD to the following\nbuild() { export CFLAGS=\u0026#34;-march=armv6zk -mcpu=arm1176jzf-s -Ofast -fno-fast-math -mfloat-abi=hard -mfpu=vfp -pipe -fomit-frame-pointer -fstack-protector --param=ssp-buff$ export CXXFLAGS=$CFLAGS cd \u0026#34;${_gitname}\u0026#34; ./configure --platform=libretro make } package() { install -Dm644 \u0026#34;${_gitname}/libretro.so\u0026#34; \u0026#34;${pkgdir}/usr/lib/libretro/libretro-pcsx-rearmed.so\u0026#34; install -Dm644 \u0026#34;pcsx_rearmed_libretro.info\u0026#34; \u0026#34;${pkgdir}/usr/lib/libretro/libretro-pcsx-rearmed.info\u0026#34; } It was running, but not full speed (some game like motoracer was around 55 Fps)\u0026hellip; the Broadcom SoC of the Pi is definitely too slow, even with my highly overclocked settings.\neduke To make it compiles, I had to edit the PKBUILD to add the arch \u0026ldquo;armv6h\u0026rdquo;, removed gtk2 and libgl dependencies (avoid useless package install as only sdl2 mode works with rpi) and then set additional parameters after the \u0026ldquo;make\u0026rdquo; call :\nUSE_OPENGL=0 POLYMER=0 NOASM=1 WITHOUT_GTK=1 LINKED_GTK=0 USE_LIBVPX=0 It compiles but\u0026hellip; it does not work with composite output, only HDMI is supported (maybe some weird hard coded things)\nuae4all Original uae4all does not run fast enought on Raspberry Pi. A dedicated version exists here : http://fdarcel.free.fr), but it needed some operations to compile without errors:\nyaourt -S guichan sdl_image libpng sdl sdl_ttf sdl_gfx wget http://fdarcel.free.fr/uae4all2-rpi-chips-0_5.bz2 tar -jxf uae4all2-rpi-chips-0_5.bz2 Then, in uae4all2 directory,\nI modified src/gui.ccp (function \u0026ldquo;void gui_handle_events (void)\u0026rdquo;) to customize controls I edited Makefile to change \u0026ldquo;g++-4.8\u0026rdquo; to \u0026ldquo;g++\u0026rdquo; compiled with : DISTCC_POTENTIAL_HOSTS=\u0026lsquo;localhost xxx.xxx.xxx.xxx\u0026rsquo; pump make -j2 CC=\u0026ldquo;distcc gcc\u0026rdquo; CXX=\u0026ldquo;distcc g++\u0026rdquo; TARGET=pi1 INCLUDE_SERVER_ARGS=\u0026rsquo;\u0026ndash;unsafe_absolute_includes\u0026rsquo; Again, it ran, but not always at full speed and the experience was pretty bad without a mouse to change game disks\u0026hellip;\nGPIO Controls The simplest way to add universal support to GPIO driven buttons, is to map them to keyboard keys. Adafruit provides a program called \u0026ldquo;retrogame\u0026rdquo; that does exactly that :\nhttps://learn.adafruit.com/retro-gaming-with-raspberry-pi/buttons https://github.com/adafruit/Adafruit-Retrogame\nI changed key setup (see retrogame.c in download section at the end of this article). To compile it:\nI downloaded the odroid-w kernel from github, copied \u0026ldquo;Adafruit-Retrogame\u0026rdquo; in /drivers updated make file to add in retrogame section right after the CC line : \u0026ldquo;arm-linux-gnueabihf-strip $@\u0026rdquo; I ran \u0026ldquo;make retrograme\u0026rdquo; Mapping some buttons to custom OS actions Running games with GPIO buttons was quite easy with Adafruit\u0026rsquo;s retrogame. But I wanted to use some buttons to do more OS based actions : turning on/off wifi and changing sound volume.\nThe solution I found is named esekeyd : it allows to map special keyboard keys to shell commands. As I already had set special keys mapping with \u0026ldquo;retrogame\u0026rdquo;, I just had to write a proper esekeyd.conf :\nVOLUMEDOWN:(killall aplay; amixer set PCM 1dB- \u0026amp;\u0026amp; aplay /root/Beep.wav) \u0026amp; VOLUMEUP:(killall aplay; amixer set PCM 1dB+ \u0026amp;\u0026amp; aplay /root/Beep.wav) \u0026amp; HOME:startwifi.sh \u0026amp; MUTE:stopwifi.sh \u0026amp; I also edited /usr/lib/systemd/system/esekeyd.service to force input to the one created by retrogame (/dev/input/event3)\nFinaly I added esekeyd service start command in /etc/rc.local in order to be sure that retrogame was loaded and correctly started before (see here for instructions to enable rc.local with systemd)\nDisplay setting Nothing special here. I used custom overscan settings to make use of every pixels and I used a 180° rotation as I mounted the screen upside down for a matter of internal space :\noverscan_left=-14 overscan_right=-10 overscan_top=-20 overscan_bottom=-20 overscan_scale=1 display_rotate=2 disable_splash=1 disable_camera_led=1 Sound setting As I\u0026rsquo;m using only one speaker together with a mono amplifier wired to one of the two audio channels, I configured a default route to make alsa merge both right and left audio channels to the left one.\nAs my ampli is a little bit too powerfull for the small speaker, I also limitted the output to 70%.\nTo do so, I created /etc/asound.conf (to reload the configuration, I issued a \u0026ldquo;systemctl restart alsa-restore\u0026rdquo;) :\npcm.!default makemono pcm.makemono { type route slave.pcm \u0026#34;hw:0\u0026#34; ttable { 0.0 0.7 # in-channel 0, out-channel 0, 70% volume 1.0 0.7 # in-channel 1, out-channel 0, 70% volume } } Finaly, i also forced audio to use the analog output (default if HDMI) in rc.local:\namixer cset numid=3 1 Battery indicator The battery indicator is based on the Odroid-w embeded PMIC controller (a Ricoh RC5T619). I wired a 6 leds breadboard to GPIO pins and used a bash script to update leds status every 2 minutes regarding the battery statuts as reported by the kernel.\nI used the wiringpi library (which provides the gpio command). Example :\nstatus=$(cat /sys/class/power_supply/battery/status) if [ \u0026#34;$status\u0026#34; == \u0026#34;Not charging\u0026#34; ]; then gpio write 0 1 gpio write 7 1 [...] Unfortunately, the PMIC driver seems to be a little bit buggy :\nThe indicated capacity (/sys/class/power_supply/battery/capacity) is not accurate. I had to \u0026ldquo;calibrate\u0026rdquo; myself the indicator using /sys/class/power_supply/battery/voltage_now (which is also false regarding the real voltage I measured\u0026hellip;). If the device is switched on with a power source plugged in, the PMIC does not return anything (that why i created a special case for the status \u0026ldquo;Not charging\u0026rdquo;) The script I created (in /usr/bin/battery.sh) is available in the download section. I added it to rc.local. I also created another script to initialise GPIO ports for all leds at start (/usr/bin/init_battery.sh):\ngpio mode 8 out gpio mode 9 out gpio mode 7 out gpio mode 0 out gpio mode 2 out gpio mode 3 out Overcloking / memory settings I have to admit that I had been lucky with the SoC on my Odroid-w. Everything run smoothly, but I used quite aggressive overclocking settings compared to what is typically stable with a Raspberry-pi. Here is what I used (so far, it never crashed event after more that 2 hours of gaming) :\narm_freq=1050 sdram_freq=600 over_voltage=6 over_voltage_sdram=2 core_freq=500 gpu_freq=300 disable_pvt=1 init_emmc_clock=325000000 gpu_mem=192 The init_emmc_clock value allowed me to push SDCard speed from 18 to 23 Mb/s (using dd to benchmark).\nOptimize / finalize After that all was working correctly, I ended the install process with the following steps.\nI disabled unwanted services / programs startup:\nsystemctl mask serial-getty@ttyAMA0.service systemctl mask remote-fs systemctl mask swap systemctl mask logrotate systemctl mask man-db systemctl mask shadow I Blacklisted some modules in /etc/modprobe/blacklist to disable ipv6 and wifi (I activate it on demand only, through hardware buttons) :\nblacklist ipv6 blacklist 8192cu I disabled all kernel output at boot in cmdline.txt (boot a little bit faster) :\nlogo.nologo quiet loglevel=0 I set the performance governor at start (rc.local)\necho \u0026#34;performance\u0026#34; \u0026gt; /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor I disabled tty1 to avoid false login attempts while pressing the \u0026ldquo;start\u0026rdquo; button which is mapped to \u0026ldquo;enter\u0026rdquo; key :\nsystemctl disable getty@tty1 Finaly, I used the EmulationStation\u0026rsquo;s scaper to enhance game listings.\nThe Result in video Download section 8192cu.conf asound.conf battery.sh Beep.wav cmdline.txt config.txt es_input.cfg es_settings.cfg es_systems.cfg esekeyd.conf init_battery.sh rc.local retroarch.cfg retrogame.c startwifi.h stopwifi.sh\n","permalink":"https://www.bluemind.org/hardware-linux-retro-boy-portable-gaming-console-odroid-w-gameboy-case/","summary":"\u003cp\u003eI have seen some guys who put a raspberry pi in a Game Boy case (eg. \u003ca href=\"https://superpiboy.wordpress.com/\"\u003ehere\u003c/a\u003e or \u003ca href=\"http://www.instructables.com/id/Gameboy-LCDRaspi-Upgrade/\"\u003ehere\u003c/a\u003e). Being a fan of retro-gaming, I wanted to do the same for me and my children. But I wanted something a little bit more polished than what I saw, I mean better looking, better battery life, etc. I discovered at the same time the \u003ca href=\"http://www.hardkernel.com/main/products/prdt_info.php?g_code=g140610189490\" title=\"Odroid-W\"\u003eOdroid-W\u003c/a\u003e (which is, unfortunately,  no more produced).\nThe following lines are about the build story\u0026hellip;\u003c/p\u003e","title":"Hardware / Linux : Retro Boy - portable gaming console with an Odroid-w and a GameBoy case"},{"content":"As explained in a previous post, I made a small custom ARM servers bay with 3 cubieboards. Until now, switching power or using UART connection for debugging (headless servers only) required physical accesses. As this server bay is in my garage and not easily accessible, I decided to build an Arduino based solution to switch on/off and debug over my network.\nThe idea was to use simple HTTP GET urls to turn on or off any server and have UART debug through telnet.\nHardware Used pieces An Arduino Nano clone (as found on ebay for 3 to 5 euros) 3 relay modules One Ethernet ENC28j60 shield Wiring Wiring ethernet shield is well documented on the web, here for example. I also added a wire from the ENC28J60 \u0026ldquo;RST\u0026rdquo; pin to the \u0026ldquo;Reset\u0026rdquo; pin of the Arduino, allowing to reset the ethernet shield when the Arduino is reseted. As for the 3 relays, it\u0026rsquo;s quite straight forward : they require 5v, ground and a wire to any free GPIO of the arduino to drive them (on or off). Each relay must then be connected to the power source that need to be switched on or off. As both Arduino and Cubieboards use 5v, I put all plus and ground together.\n[caption id=\u0026ldquo;attachment_711\u0026rdquo; align=\u0026ldquo;alignleft\u0026rdquo; width=\u0026ldquo;150\u0026rdquo;] All together wired[/caption]\n[caption id=\u0026ldquo;attachment_712\u0026rdquo; align=\u0026ldquo;alignleft\u0026rdquo; width=\u0026ldquo;150\u0026rdquo;] The Arduino nano wired (Only one UART connection on brown and orange wires)[/caption]\n[caption id=\u0026ldquo;attachment_713\u0026rdquo; align=\u0026ldquo;alignleft\u0026rdquo; width=\u0026ldquo;150\u0026rdquo;] The ENC28J60 ethernet shield wired[/caption]\n[caption id=\u0026ldquo;attachment_714\u0026rdquo; align=\u0026ldquo;alignleft\u0026rdquo; width=\u0026ldquo;150\u0026rdquo;] the 3 relays wired[/caption]\nArduino Sketch Preamble I used 2 libraries : SoftwareSerial and UIPEthernet. With debug deactivated and DHCP enabled, the whole sketch take less than 28,000 bytes of the 30,720 bytes availables : it fits perfectly the ATMega 328. The Sketch provides the following functionalities:\nSupport for 3 servers / 3 UART connections Static or DHCP based IP address Basic HTTP server to switch power and set which UART to use over Telnet connection Any HTTP order will answer with an XML document that gives actual statuses Telnet Server with UART/Serial to Telnet gateway Custom DEBUG Macro to make the code easier to use / maintain To give a simple overview, the usual Setup() and Loop() functions are self explanatory: we initialize the hardware and network, start HTTP and Telnet server listener. Then the main loop just wait / handle HTTP and telnet connections.\nvoid setup() { #if DEBUG == 1 Serial.begin(9600); #endif _MSGLN(\u0026#34;starting system...\u0026#34;); initSwitches(); initNetwork(); initServers(); setCurrentSoftSerial(SERVER1_RX, SERVER1_TX); _MSGLN(\u0026#34;Waiting for connections\u0026#34;); } void loop() { handleTelnet(); handleHttp(); } In the following lines, I won\u0026rsquo;t explain :\nethernet initialization : the official documentation tells enough, switches relay initialization : it\u0026rsquo;s Arduino very basic things ( pinMod() and digitalWrite() ) HTTP server part what it does The HTTP part is responsible for the user interface. It takes orders through HTTP queries and convert them into Arduino instructions :\nSwitch on or off one or all the 3 servers (eg : http:///SERVER1/ON, http:///ALL/OFF) Set which UART connection to use through the telnet listener (eg : http:///SERVER2/TELNET) The Query parser The Query Parser is very minimalist in order to let the micro-controller able to quikly handle UART, Telnet, XML rendering and orders at the \u0026ldquo;same time\u0026rdquo;. Basicaly, the HTTP parser functionnalities and limitations are:\nMax 128 characters No HTTP order type check (put, delete, post, get or whatever will produce the same result) No HTTP header support : it only read the query on the very first line (see HTTP Rfc) HTTP protocol version is ignored As soon as a query is detected, it searches for action to do ( handleHttpAction() ) and then renders the XML status report ( handleHttpHome() ) void handleHttp() { EthernetClient client = HttpServer.available(); if (client) { _MSGLN(\u0026#34;HTTP connection detected !\u0026#34;); char c; int i; // first loop : ignore request type for(i=0, c = client.read(); client.available() \u0026amp;\u0026amp; c != \u0026#39; \u0026#39; \u0026amp;\u0026amp; i \u0026lt; HTTP_BUFFER_SIZE ; i++, c = client.read()) { _MSG(\u0026#34;1 read : \u0026#34;) ; _VARLN(c); } // second loop : get the URI for(i=0, c = client.read(); client.available() \u0026amp;\u0026amp; c != \u0026#39; \u0026#39; \u0026amp;\u0026amp; c != \u0026#39;\\n\u0026#39; \u0026amp;\u0026amp; i \u0026lt; HTTP_BUFFER_SIZE ; i++, c = client.read()) { _MSG(\u0026#34;2 read \u0026#34;) ; _VAR(i) ; _MSG(\u0026#34; : \u0026#34;) ; _VARLN(c); HttpReqBuff[i] = c; } HttpReqBuff[i] = \u0026#39;\\0\u0026#39;; _MSG(\u0026#34;Final string : \u0026#34;); _VARLN(HttpReqBuff); handleHttpAction(); handleHttpHome(\u0026amp;client); delay(1); client.stop(); _MSGLN(\u0026#34;HTTP connection closed\u0026#34;); } } http action handler The Action handler tries to detect an instruction from the \u0026ldquo;just received\u0026rdquo; query. It uses strcmp_P() so query can be compared against char arrays that are stored in program space (using PSTR macro). This allows to preserve RAM by not storing all non dynamic strings in it.\n// turn switches ON or OFF or set current serial connection depending on HTTP query static void handleHttpAction() { _MSGLN(\u0026#34;handle http action\u0026#34;); if(!strcmp_P(HttpReqBuff, PSTR(\u0026#34;/SERVER1/ON\u0026#34;)) || !strcmp_P(HttpReqBuff, PSTR(\u0026#34;/ALL/ON\u0026#34;))) digitalWrite(SERVER1, LOW); if(!strcmp_P(HttpReqBuff, PSTR(\u0026#34;/SERVER1/OFF\u0026#34;)) || !strcmp_P(HttpReqBuff, PSTR(\u0026#34;/ALL/OFF\u0026#34;))) digitalWrite(SERVER1, HIGH); if(!strcmp_P(HttpReqBuff, PSTR(\u0026#34;/SERVER2/ON\u0026#34;)) || !strcmp_P(HttpReqBuff, PSTR(\u0026#34;/ALL/ON\u0026#34;))) digitalWrite(SERVER2, LOW); if(!strcmp_P(HttpReqBuff, PSTR(\u0026#34;/SERVER2/OFF\u0026#34;)) || !strcmp_P(HttpReqBuff, PSTR(\u0026#34;/ALL/OFF\u0026#34;))) digitalWrite(SERVER2, HIGH); if(!strcmp_P(HttpReqBuff, PSTR(\u0026#34;/SERVER3/ON\u0026#34;)) || !strcmp_P(HttpReqBuff, PSTR(\u0026#34;/ALL/ON\u0026#34;))) digitalWrite(SERVER3, LOW); if(!strcmp_P(HttpReqBuff, PSTR(\u0026#34;/SERVER3/OFF\u0026#34;)) || !strcmp_P(HttpReqBuff, PSTR(\u0026#34;/ALL/OFF\u0026#34;))) digitalWrite(SERVER3, HIGH); if(!strcmp_P(HttpReqBuff, PSTR(\u0026#34;/SERVER1/TELNET\u0026#34;))) { setCurrentSoftSerial(SERVER1_RX, SERVER1_TX); return; } if(!strcmp_P(HttpReqBuff, PSTR(\u0026#34;/SERVER2/TELNET\u0026#34;))) { setCurrentSoftSerial(SERVER2_RX, SERVER2_TX); return; } if(!strcmp_P(HttpReqBuff, PSTR(\u0026#34;/SERVER3/TELNET\u0026#34;))) { setCurrentSoftSerial(SERVER3_RX, SERVER3_TX); return; } } result handler Any HTTP request (valid or not) will result in the same status reporting content. This content simply consists in an XML result that shows relay statuses as well as which server\u0026rsquo;s UART port is actually used for the telnet gateway. The check of the currently used UART is based on one of the 2 UART pins, for instance I arbitrarily used the RX pin.\nvoid handleHttp() { EthernetClient client = HttpServer.available(); if (client) { _MSGLN(\u0026#34;HTTP connection detected !\u0026#34;); char c; int i; // first loop : ignore request type for(i=0, c = client.read(); client.available() \u0026amp;\u0026amp; c != \u0026#39; \u0026#39; \u0026amp;\u0026amp; i \u0026lt; HTTP_BUFFER_SIZE ; i++, c = client.read()) { _MSG(\u0026#34;1 read : \u0026#34;) ; _VARLN(c); } // second loop : get the URI for(i=0, c = client.read(); client.available() \u0026amp;\u0026amp; c != \u0026#39; \u0026#39; \u0026amp;\u0026amp; c != \u0026#39;\\n\u0026#39; \u0026amp;\u0026amp; i \u0026lt; HTTP_BUFFER_SIZE ; i++, c = client.read()) { _MSG(\u0026#34;2 read \u0026#34;) ; _VAR(i) ; _MSG(\u0026#34; : \u0026#34;) ; _VARLN(c); HttpReqBuff[i] = c; } HttpReqBuff[i] = \u0026#39;\\0\u0026#39;; _MSG(\u0026#34;Final string : \u0026#34;); _VARLN(HttpReqBuff); handleHttpAction(); handleHttpHome(\u0026amp;client); delay(1); client.stop(); _MSGLN(\u0026#34;HTTP connection closed\u0026#34;); } } Note: for relay statuses, I wanted 0 for off and 1 for on. Relay statuses being the contrary, this explains the \u0026ldquo;!\u0026rdquo; in front of digitalRead().\nExample of XML output :\n\u0026lt;ROOT\u0026gt; \u0026lt;SERVER1\u0026gt;1\u0026lt;/SERVER1\u0026gt; \u0026lt;SERVER2\u0026gt;0\u0026lt;/SERVER2\u0026gt; \u0026lt;SERVER3\u0026gt;1\u0026lt;/SERVER3\u0026gt; \u0026lt;SERIAL\u0026gt;SERVER1\u0026lt;/SERIAL\u0026gt; \u0026lt;/ROOT\u0026gt; Serial / UART and Telnet part Multiple UART connections Each cubieboard (1 and 2) has an UART port which is the default output. I first tried to instantiate a softwareSerial for each\u0026hellip; but it was eating too much memory and things was way to slow even for a 9600 bauds rate. So the solution was simply to use one softwareSerial at a time and delete / instanciate required one regarding an HTTP query. The \u0026ldquo;current\u0026rdquo; softwareSerial instance is accessed with a global pointer. Changing instance is done via the following function :\nvoid setCurrentSoftSerial(int RX, int TX) { CurrentRXPin = RX; if(CurrentSoftSerial) delete(CurrentSoftSerial); CurrentSoftSerial = new SoftwareSerial(RX, TX); CurrentSoftSerial-\u0026gt;begin(TELNET_SPEED); _MSG(\u0026#34;Software serial changed : RX = \u0026#34;); _VARLN(CurrentRXPin); } UART to Serial geteway Even at a 9600 bauds rate, reading from serial connection and writing directly to ethernet driver generated trashed and/or missing characters. It may be due to the Ethernet driver and/or hardware that need some in-compressible delay to send data\u0026hellip; So I had to use a custom buffer of 64 bytes. The buffer is a global char array. Below are the gateway and the buffer writer functions.\nvoid handleTelnet() { EthernetClient client = TelnetServer.available(); char c; // if something was typed through telnet, send it to serial connection if(client) { _MSGLN(\u0026#34;send serial char\u0026#34;); CurrentSoftSerial-\u0026gt;write(client.read()); } // handle incomming byte from serial connection and write it to telnet connection // buffering is mandatory to not loose any byte. while(CurrentSoftSerial-\u0026gt;available()) { c = CurrentSoftSerial-\u0026gt;read(); if(c \u0026lt; 0) continue; SerialBuffer[SerialBufferPos] = c; //_MSG(\u0026#34;Getting char \u0026#34;); _VAR(SerialBufferPos); _MSG(\u0026#34; :\u0026#34;) ; _VARLN(SerialBuffer[SerialBufferPos-1]); // send data as soon as buffer is full if(SerialBufferPos==SERIAL_BUFFER_SIZE-1) WriteSerialBufferToTelnet(SERIAL_BUFFER_SIZE); SerialBufferPos++; } // handle case where no more data available, but still some chars left in buffer WriteSerialBufferToTelnet(SerialBufferPos); } void WriteSerialBufferToTelnet(int sizeToWrite) { if(sizeToWrite \u0026gt; 0 \u0026amp;\u0026amp; sizeToWrite \u0026lt;= SERIAL_BUFFER_SIZE) { TelnetServer.write(SerialBuffer, sizeToWrite); _VARLN(SerialBuffer); SerialBufferPos=0; } } Cubieboards side SoftwareSerial allows to do serial connection through arduino GPIO pins\u0026hellip; in software, as its name suggests. If using a 57600 bauds rate connection might be achievable with an fully dedicated Arduino for this task, the best I could get overall is 9600 bauds. As default cubieboard\u0026rsquo;s uboot and environment\u0026rsquo;s variables are set to communicate with a bauds rate of 57600, it was necessary to compile my own u-boot. I did that on the cubieboard itself, but this can be of course be cross-compiled :\ngit clone git://git.denx.de/u-boot.git cd u-boot echo \u0026#34;CONFIG_BAUDRATE=9600\u0026#34; \u0026gt;\u0026gt; configs/Cubieboard_defconfig make Cubieboard_defconfig make -j3 I then flashed it from my laptop on the Cubieboard SdCard :\ndd if=u-boot-sunxi-with-spl.bin of=/dev/sdb bs=1024 seek=8 env var settings Now U-Boot allows the cubieboard to boot with the correct bauds rate\u0026hellip;. BUT, as soon as the environment variables are read from the SDCard, it switches to 57600 again. Using \u0026ldquo;setenv baudrate 9600\u0026rdquo; require to disconnect the current connection and reconnect with 9600, then press \u0026ldquo;enter\u0026rdquo; (send CR/LF) to validate the change. I also, changed some other parameters to be sure that any recent kernel will boot in 9600 bauds rates (\u0026ldquo;machid\u0026rdquo; being needed for cubieboard 1 only)\nsetenv machid 1008 setenv console ttyS0,9600 saveenv reset Change getty Finaly, change /etc/inittab so getty also run at 9600 bauds :\nT0:2345:respawn:/sbin/getty -L ttyS0 9600 linux Upgraded cookie box I now have a nice solution to switch on or off any of my 3 cubieboards with no physical access\u0026hellip; with debugging support through telnet ! Well, from time to time, the Arduino (or at least the ethernet interface) seems to crash while connected to telnet after a random (but large) amount of data.\nRegarding the security of this system, it must be done at lan / switch / firewall level, as this solution does not offer any protection nor encryption (it may be possible to do some encryption). It is also not recommended to login using telnet: the purpose is more to be able to debug the boot sequence, not to use as a daily console\u0026hellip;\nFull Source code : ServerSwitches2 Note that UIPEthernet library must be modified, see Problems with ENC28J60\n","permalink":"https://www.bluemind.org/arduino-http-driven-power-switches-uart-telnet-converter/","summary":"\u003cp\u003eAs explained in a \u003ca href=\"/hardware-cookie-box-host-3-cubieboards/\" title=\"Hardware : a cookie box to host 3 cubieboards\"\u003eprevious post\u003c/a\u003e, I made a small custom ARM servers bay with 3 cubieboards. Until now, switching power or using UART connection for debugging (headless servers only) required physical accesses. As this server bay is in my garage and not easily accessible, I decided to build an Arduino based solution to switch on/off and debug over my network.\u003c/p\u003e\n\u003cp\u003eThe idea was to use simple HTTP GET urls to turn on or off any server and have UART debug through telnet.\u003c/p\u003e","title":"Arduino : HTTP driven power switches and UART to telnet gateway"},{"content":"Here is some notes of my own Zabbix monitoring system installation (debian based). It has been done since a while now, but I never took the time to finalize this post. However, it can still be usefull as not much (nothing ?) has changed regarding activities to do\u0026hellip;\nBase Install Server Part apt-get install apache2 mysql snmp zabbix-agent zabbix-frontend-php zabbix-server-mysql Snmp is of course only usefull if you have things that support it (my administrable switch does)\u0026hellip;\nThen follow instruction on /usr/share/doc/zabbix-server-mysql/README.Debian\nIn /etc/zabbix/zabbix_server.conf,\nI comment out \u0026ldquo;LogFile\u0026rdquo; to use Syslog as I centralise all my logs with rsyslog I set Housekeeping to 24 (small home network with less than 10 servers) LogSlowQuery=10 Enable zabbix-server in /etc/default/zabbix-server and launch it:\n/etc/init.d/zabbix-server start Make the admin console available through an alias in apache and restrict access to some IP addresses (vhost should work too) :\n\u0026lt;Directory /usr/share/zabbix\u0026gt; Options FollowSymLinks AllowOverride None Order allow,deny Allow from a.b.c \u0026lt;/Directory\u0026gt; \u0026lt;IfModule mod_alias.c\u0026gt; Alias /zabbix /usr/share/zabbix \u0026lt;/IfModule\u0026gt; Put this in sites-available instead (I prefer that over the conf directory, then activate the site (either a2ensite or simple ln -s) and finaly restart apache (apachectl graceful).\nThen, go to https:///zabbix and follow the instructions. At the end of the procedure, download and copy the generated configuration file to /etc/zabbix and set restrictive permission on it (db password in clear text inside) :\nchown root:www-data /etc/zabbix/zabbix_server.conf chmow 640 /etc/zabbix/zabbix_server.conf Be careful: you may need to increase the number of MySQL connections (max_connection parameter) if you already have other services that use it (zabbix server + Php front-end eat up at least 10 connections for a single user).\nAlso, mysqltunner can be very usefull to check MySQL settings.\nClient Part (agent) If all is up and running, zabbix-agent must be installed on all server that need to be monitored (apt-get install zabbix-agent). Then :\nset hostname set zabbix server ip optionally comment out LogFile to use syslog (rsyslog in my case) update /etc/hosts.allow (eg: zabbix_agentd:) Custom Monitoring HTTPS Urls if monitoring of https services fail, change the standard item from \u0026ldquo;net.tcp.service[https]\u0026rdquo; to \u0026ldquo;net.tcp.service[tcp,,443]\u0026rdquo;\nApache You can find great bash scripts to monitor apache variables here : https://www.zabbix.org/wiki/Docs/howto/apache_monitoring_script\ncopy them on /usr/share/zabbix-scripts/ and give proper permissions\nchmod 770 /usr/share/zabbix-scripts/apache_report.sh chown zabbix /usr/share/zabbix-scripts/apache_report.sh Nginx Example can be found here : http://www.badllama.com/content/monitor-nginx-zabbix\nIf you want max supported connection on your screens, add the following \u0026ldquo;userparameter\u0026rdquo; :\nUserParameter=nginx.maxconnection[*],grep -o \u0026#34;worker_[[:alpha:]]*[[:blank:]]*[[:digit:]]*[[:blank:]]*;\u0026#34; $1 | tr -d \u0026#39;\\n\u0026#39; | tr \u0026#39;;\u0026#39; \u0026#39; \u0026#39; | awk \u0026#39;{print $$2 * $$4}\u0026#39; In case it may help, my own Nginx template : zbx_export_templates\nMySQL see https://www.zabbix.com/forum/showthread.php?t=41659\nCreate a user for zabbix that is allowed to check MySQL processes. For example :\nGRANT PROCESS ON * . * TO \u0026#39;zabbix\u0026#39;@\u0026#39;localhost\u0026#39; IDENTIFIED BY \u0026#39;***\u0026#39; WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0 MAX_USER_CONNECTIONS 0 ; Then protect the file : owned by the user and group your zabbix agent is running on behalf of and proper chmod :\nchown zabbix:zabbix userparameter_mysql.conf chmod 400 userparameter_mysql.conf You can also set a home dir for zabbix user, and set user and password in ~/.my.cnf\nSamba See : https://www.zabbix.com/forum/showthread.php?t=1995\nWeekly and monthly Screen summary by e-mail You can find a nice Perl script that send an e-mail with all of your favorites Zabbix screen here : https://www.zabbix.com/forum/showthread.php?t=20312\nTo make it work as, for example, /usr/share/zabbix-scripts/graphreport.pl you need the following :\napt-get install curl mkdir /usr/share/zabbix-scripts /usr/bin/perl -MCPAN -e \u0026#39;install DateTime\u0026#39; /usr/bin/perl -MCPAN -e \u0026#39;install MIME::Lite\u0026#39; chmod 700 /usr/share/zabbix-scripts/graphreport.pl You need also to adapt parameters at the begening of the file regarding Zabbix user and database\nIn addition, if you are using Zabbix 2.2.1, change the line 62 (\u0026quot;$cmglogin..\u0026quot;.) by :\nmy $cmdLogin = `curl -s -c $cookie -d \u0026#39;name=$login\u0026amp;password=$pass\u0026amp;enter=Sign%20in\u0026#39; $zabbix/index.php`; ","permalink":"https://www.bluemind.org/linux-monitoring-supervision-zabbix/","summary":"\u003cp\u003eHere is some notes of my own Zabbix monitoring system installation (debian based). It has been done since a while now, but I never took the time to finalize this post. However, it can still be usefull as not much (nothing ?) has changed regarding activities to do\u0026hellip;\u003c/p\u003e\n\u003ch1 id=\"base-install\"\u003eBase Install\u003c/h1\u003e\n\u003ch2 id=\"server-part\"\u003eServer Part\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eapt-get install apache2 mysql snmp zabbix-agent zabbix-frontend-php zabbix-server-mysql\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSnmp is of course only usefull if you have things that support it (my administrable switch does)\u0026hellip;\u003c/p\u003e","title":"Linux : Monitoring And supervision with Zabbix"},{"content":"As an owner of the excellent Linksys (Cisco) E4200 router, I use a Debian Linux distribution on it (more info on http://www.wolfteck.com/projects/candyhouse/).\nUnfortunately, the given 3.13.7 kernel does not provide cifs module to mount SMB share. As I need it to for my automated monthly backup, I compiled it myself : you can find it in the download section of this post.\nHow to use it (well if you read this post you probably know how to\u0026hellip;):\ncreate the dir /lib/modules/3.13.7/kernel/fs/cifs unzip and copy cifs.ko in this directory add module with insmod (insmod /lib/modules/3.13.7/kernel/fs/cifs/cifs.ko Beware, to use it you need to install cifs-utils (eg: apt-get install cifs-utils)\n","permalink":"https://www.bluemind.org/linux-cifs-module-linksys-e4200-candyhouse-debian-kernel/","summary":"\u003cp\u003eAs an owner of the excellent Linksys (Cisco) E4200 router, I use a Debian Linux distribution on it (more info on \u003ca href=\"http://www.wolfteck.com/projects/candyhouse/)\"\u003ehttp://www.wolfteck.com/projects/candyhouse/)\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eUnfortunately, the given 3.13.7 kernel does not provide cifs module to mount SMB share. As I need it to for my automated monthly backup, I compiled it myself : you can find it in the download section of this post.\u003c/p\u003e\n\u003cp\u003eHow to use it (well if you read this post you probably know how to\u0026hellip;):\u003c/p\u003e","title":"Linux : CIFS module for linksys E4200 (candyhouse) debian kernel"},{"content":"After some crashes running OpenHab on one of my cubieboards with a freshly installed Oracle\u0026rsquo;s JRE 1.8 and several tests, I started to suspect the kernel.\nIt was time to test a new compiled one, more recent, based on linux-sunxi project, with no additional CCFLAGS (I suspect that enforcing hard float through neon is the culprit). Here it is ! No more crash on my side now.\nThese are just kernels for headless cubieboard servers (no mali, no g2d, very few usb devices, etc\u0026hellip;), no modules and maximun available RAM.\nAll my cubieboards run fine 24/7 since several days with these kernels (debian sid), DRam @ 480 Mhz (cas 6) and CPU @ 1.01Ghz. Putting higher frequency on dram and/or cpu has led to crash under stress.\n","permalink":"https://www.bluemind.org/linux-cubieboard-1-2-server-optimized-custom-kernel-3-4-103/","summary":"\u003cp\u003eAfter some crashes running OpenHab on one of my cubieboards with a freshly installed Oracle\u0026rsquo;s JRE 1.8 and several tests, I started to suspect the kernel.\u003c/p\u003e\n\u003cp\u003eIt was time to test a new compiled one, more recent, based on \u003ca href=\"https://github.com/linux-sunxi/linux-sunxi\"\u003elinux-sunxi\u003c/a\u003e project, with no additional CCFLAGS (I suspect that enforcing hard float through neon is the culprit). Here it is ! No more crash on my side now.\u003c/p\u003e\n\u003cp\u003eThese are just kernels for headless cubieboard servers  (no mali, no g2d, very few usb devices, etc\u0026hellip;), no modules and maximun available RAM.\u003c/p\u003e","title":"Linux : cubieboard 1 \u0026 2 headless server optimized custom kernel 3.4.103"},{"content":"As an home automation passionate, I have some parts of my home which are automated (heating, lights, roller shutter,\u0026hellip;).\nI\u0026rsquo;m using a French box from Zodianet which name is Zibase since more than 2 years now. This box as a great advantage for its price : it recognizes a lot of protocols (433 Mhz, Xdd, ZWave, enOcean, visionic 868, X10).\nBUT, the only way to program it is to use a Cloud based interface which is awfull and not very flexible\u0026hellip; at least regarding solutions like OpenHab.\nThat\u0026rsquo;s why I created a binding for Openhab, allowing to bypass the cloud interface and use the zibase as a kind of \u0026ldquo;Rf Router\u0026rdquo; for OpenHab.\nThe code is actualy in the official GitHub pull requests waiting list : https://github.com/openhab/openhab/pull/1684\nThe fork I created can be viewed here : https://github.com/jit06/openhab\nWhile the binding\u0026rsquo;s source code is being reviewed by openhab authors, I propose the binding for downloading here (see upper part of the right side bar).\nDocumentation can be found on my fork : https://github.com/jit06/openhab/wiki/Zibase-Binding\nAny feedback would be appreciated, in particular for testing zibase 2, zibase PRO, an all sort of sensors I don\u0026rsquo;t have.\n","permalink":"https://www.bluemind.org/project-openhab-binding-zibase/","summary":"\u003cp\u003eAs an home automation passionate, I have some parts of my home which are automated (heating, lights, roller shutter,\u0026hellip;).\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;m using a French box from Zodianet which name is Zibase since more than 2 years now. This box as a great advantage for its price : it recognizes a lot of protocols (433 Mhz, Xdd, ZWave, enOcean, visionic 868, X10).\u003c/p\u003e\n\u003cp\u003eBUT, the only way to program it is to use a Cloud based interface which is awfull and not very flexible\u0026hellip; at least regarding solutions like \u003ca href=\"http://www.openhab.org/\"\u003eOpenHab\u003c/a\u003e.\u003c/p\u003e","title":"Project : Openhab binding for Zibase"},{"content":"In order to better protect my cubieboards servers and use less power adapters, I built a small cluster home using a (smurf) cookie box\u0026hellip; run pretty well, plus it dissipates nicely the heat !\nPhotos are here\n","permalink":"https://www.bluemind.org/hardware-cookie-box-host-3-cubieboards/","summary":"\u003cp\u003eIn order to better protect my cubieboards servers and use less power adapters, I built a small cluster home using a (smurf) cookie box\u0026hellip; run pretty well, plus it dissipates nicely the heat !\u003c/p\u003e\n\u003cp\u003ePhotos are \u003ca href=\"/misc-galleries/?album=1\u0026amp;gallery=4\"\u003ehere\u003c/a\u003e\u003c/p\u003e","title":"Hardware : a cookie box to host 3 cubieboards"},{"content":"I updated my cubieboards 1 and 2 kernels a few weeks ago. The goal was to optimize them for headless server (low power and maximum memory). Priority was : few modules, no GPU, no G2D, minimal embedded set of drivers (basically usb, gpio, ethernet, sdcard).\nEverything run fine since several weeks (24/7), so I decided to share these kernels as well as associated modules: it may interest some peopole. I took the source at http://www.danand.de/index.php/2014-07/linux-3-4-97-for-allwinner-a20-boards/\nI also provide headless server optimized Script.bin files. They mainly put dram clock to 480 Mhz and disable anything not needed (hdmi, mali GPU, etc.)\nBeware that kernel boot arguments have been forced at compile time to the following :\nconsole=ttyS0,115200 sunxi_g2d_mem_reserve=0 sunxi_ve_mem_reserve=0 sunxi_fb_mem_reserve=0 sunxi_no_mali_mem_reserv e root=/dev/mmcblk0p2 rootwait rootfstype=ext4 Below are compilation flags I used (see https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html)\ncubiboard 2 (cb2) export CFLAGS=\u0026#34;-mthumb -march=armv7-a -mfloat-abi=hard -mfpu=neon-vfpv4 -mcpu=cortex-a7 -mtune=cortex-a7 -O3 -funroll-loop -funsafe-math-optimizations\u0026#34; cubieboard 1 (cb) export CFLAGS=\u0026#34;-mthumb -march=armv7-a -mfloat-abi=hard -mfpu=vfpv3 -mcpu=cortex-a8 -mtune=cortex-a8 -O3 -funroll-loop\u0026#34; ","permalink":"https://www.bluemind.org/linux-cubieboard-1-2-custom-kernel-headless-server/","summary":"\u003cp\u003eI  updated my cubieboards 1 and 2 kernels a few weeks ago. The goal was to optimize them for headless server (low power and maximum memory).\nPriority was : few modules, no GPU, no G2D, minimal embedded set of drivers (basically usb, gpio, ethernet, sdcard).\u003c/p\u003e\n\u003cp\u003eEverything run fine since several weeks (24/7), so I decided to share these kernels as well as associated modules: it may interest some peopole.\nI took the source at \u003ca href=\"http://www.danand.de/index.php/2014-07/linux-3-4-97-for-allwinner-a20-boards/\"\u003ehttp://www.danand.de/index.php/2014-07/linux-3-4-97-for-allwinner-a20-boards/\u003c/a\u003e\u003c/p\u003e","title":"Linux : cubieboard  1 \u0026 2 custom kernel for headless server "},{"content":"Centralizing servers and applications logs is a good way to make search and monitoring easier. Coupled with a web-interface it simplifies access (think also about sharing logs\u0026hellip;)\nMoreover, as all my servers are based on arm mini-pc (cubieboard) with flash drive, Iogs are volatile (tmpfs) to maximize SdCards life. So centralizing logs on a harddrive is a must to keep history in case of failure.\nBelow is the recipe of how I did it on all my servers (cubieboards + debian testing)\nRSYSLOG Server installation Centralizing implies having a specific server for that (or severals if you need failover). I choosed to store logs in mysql so its easier to query / view / use a web interface\u0026hellip;\nPackages installation apt-get install rsyslog rsyslog-mysql mysql-server mysql-client You will get prompted to change the root password of mysql (via dpkg-reconfigure)\nThe same for rsyslog-mysql plugin : give root mysql password, then enter a password for rsyslog user or let the system choose a random one\u0026hellip;\nConfigure rsyslog Edit /etc/rsyslog.conf to activate udp and tcp connections for (futur) rsyslog clients :\n# provides UDP syslog reception $ModLoad imudp $UDPServerRun 514 # provides TCP syslog reception $ModLoad imtcp $InputTCPServerRun 514 Then (re)start rsyslog\n$ /etc/init.d/rsyslog restart [ ok ] Stopping enhanced syslogd: rsyslogd. [ ok ] Starting enhanced syslogd: rsyslogd. Install and configure the web interface : loganalyser LAMP stack loganalyser is written in PHP and officialy support apache. To keep things simple and straighforward, let use a classical LAMP architecture. For security reasons, it is advised to make the log server only available from the lan and/or for a restricted ip range (and for professional use or big network, I would also add through https + authentication).\napt-get install libapache2-mod-php5 apt-get install php5-mysql # install mysql extension for php (needed by loganalyser) apt-get install php-apc # install APC to make php faster (opcode cache) apt-get install php5-gd # install gd extension for graphics in loganalyser loganalyser deployment cd /var/www wget http://download.adiscon.com/loganalyzer/loganalyzer-3.6.4.tar.gz tar -zxf loganalyzer-3.6.4.tar.gz ln -s loganalyzer-3.6.4 loganalyzer rm loganalyzer-3.6.4.tar.gz chown -R www-data:www-data loganalyser/src Note that we need to temporary allow write access to apache for the installation process\u0026hellip;\nApache configuration Now that loganalyser is deployed, we need to configure a virtualhost in apache. Create the conf file in /etc/apache2/sites-available/loganalyser.conf\n\u0026lt;Directory /var/www/loganalyser/src\u0026gt; Options FollowSymLinks AllowOverride All Order allow,deny Allow from \u0026lt;your lan ip address range\u0026gt; \u0026lt;/Directory\u0026gt; \u0026lt;VirtualHost *:80\u0026gt; ServerAdmin webmaster@localhost ServerName foo.bar.com DocumentRoot /var/www/loganalyzer/src ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined \u0026lt;/VirtualHost\u0026gt; Then go to /etc/apache/site-enabled, remove the default host and activate loganalyser one\nrm 000-default.conf ln -s ../sites-available/loganalyser.conf . /etc/init.d/apache2 restart LogAnalyser configuration Initial settings Point your browser to the server ip adress and follow the instructions (which begin by a \u0026ldquo;fatal error screen\u0026rdquo;\u0026hellip;) Everything is explained in the installation doc : http://loganalyzer.adiscon.com/doc/install.html\nMain choice I set (personal taste\u0026hellip;) :\nMessage character limit for the main view =\u0026gt; 0 to view full message inline Show message details popup =\u0026gt; no (because I want-it inline) Enable User Database =\u0026gt; yes I use the same database as rsyslog for loganalyser own table Rsysllog database informations are found in /etc/rsyslog.d/mysql.conf, it look-likes the following :\n$ModLoad ommysql *.* :ommysql:localhost,Syslog,\u0026lt;user\u0026gt;,\u0026lt;password\u0026gt; If all went fine, the installer will create tables prefixed with \u0026ldquo;logcon_\u0026rdquo; (if you did not modified this) and will ask to you create a user.\nConfigure a first source LogAnaylser need to know from where it should read logs to display. For now, we just want to check that everything that will be logged will be displayed, so we create a simple source :\nName = \u0026ldquo;All\u0026rdquo; Type = PDO View = Syslog Databse storage engine =\u0026gt; MySQL table type = monitorWare Fill host, tablename, user and password according to /etc/rsyslog.d/mysql.conf (beware, all is case sensitive) Ok, we have now a base installation that should already display the server\u0026rsquo;s logs. Don\u0026rsquo;t forget to remove write access to user www-data to /var/www/loganalyser/src.\nMySQL tunning As the logserver could receive an huge amount of data to write into tables, you should ajust mysql parameters after a fews days / weeks of usage.\nBasicaly :\nuse \u0026ldquo;mysqltunner\u0026rdquo; to check settings from time to time drefagment / optimize table at least once a week (mysqlcheck -o \u0026ndash;all-databases -u root -pxxxxxxx) Configures other servers to send their logs to the log server Note that for each server and/or service, you can create a dedicated source in LogAnalyser to make browsing and searching easier\u0026hellip;\nBase logging : replace syslog by rsyslog This part is the easiest one. Firstly, install rsyslog :\napt-get install rsyslog Then, put the following at the end of /etc/rsyslog.conf:\n*.* @ip.adress.of.rsyslog.server Restart rsyslog (or kill -HUP), and then every message for syslog will be sent to the log server.\nSending Apache log to rsylog Apache use a proprietary log system that write on its own files. To send them to rsyslog we need to use the \u0026ldquo;imfile\u0026rdquo; module of rsyslog.\nEdit /etc/rsyslog.conf to add file support :\n$ModLoad imfile # provide file support Then, create a configuration file for apache (I personnaly create one file per virtualhost), in /etc/rsyslog.d/apache_.conf (You can find more information on http://www.rsyslog.com/using-the-text-file-input-module)\n########## Error loggin ########### $InputFileName /var/log/apache2/\u0026lt;filename_of_your_error_log\u0026gt; $InputFileTag apache-\u0026lt;vhostname\u0026gt; $InputFileStateFile apache_\u0026lt;vhostname\u0026gt;_error $InputFileSeverity error $InputFileFacility local1 $InputRunFileMonitor ######### Access loggin ########## $InputFileName /var/log/apache2/\u0026lt;filename_of_your_access_log\u0026gt; $InputFileTag apache-\u0026lt;vhostname\u0026gt; $InputFileStateFile apache_\u0026lt;vhostname\u0026gt;_access $InputFileSeverity info $InputFileFacility local2 $InputRunFileMonitor Set Exim4 to log in syslog Edit /etc/exim4/exim4.conf.template, and add the folowing after \u0026ldquo;main/02_exim4-config_options\u0026rdquo;\nlog_file_path = syslog Then restart exim (/etc/init.d/exim4 restart) and check if its ok :\nexim4 -bP log_file_path You should read \u0026ldquo;log_file_path = syslog\u0026rdquo;\nIf it does not work, try to put the \u0026ldquo;log_file_path\u0026rdquo; setting in \u0026ldquo;etc/exim4/conf.d/main/02_exim4-config_options\u0026rdquo;\n","permalink":"https://www.bluemind.org/linux-centralized-logs-rsyslog/","summary":"\u003cp\u003eCentralizing servers and applications logs is a good way to make search and monitoring easier. Coupled with a web-interface it simplifies access (think also about sharing logs\u0026hellip;)\u003c/p\u003e\n\u003cp\u003eMoreover, as all my servers are based on arm mini-pc (cubieboard) with flash drive, Iogs are volatile (tmpfs) to maximize SdCards life. So centralizing logs on a harddrive is a must to keep history in case of failure.\u003c/p\u003e\n\u003cp\u003eBelow is the recipe of how I did it on all my servers (cubieboards  + debian testing)\u003c/p\u003e","title":"Linux : Centralized logs + web interface (aka rsyslog + loganalyser)"},{"content":"I recently upgraded one of my cubieboard by a cubieboard 2 (webserver). The goal was to keep everything else but the motherboard\u0026hellip; This post describes main necessary steps to do such a change as quickly and flawlessly as possible (so no custom kernel here\u0026hellip;).\nThe board I replaced run debian Sid (jessie) on an microSD with kernel 3.0. This microSD is detected as /dev/sdb on the laptop I use to upgrade.\nBootloader and Script.bin The cubieboard 2 use a A20 SoC instead A10. So the uBoot , script.bin and kernel are differents. I retreived these files from http://androtab.info/cubieboard2.\nwget http://files.androtab.info/allwinner/cubieboard2/u-boot.bin wget http://files.androtab.info/allwinner/cubieboard2/script.bin wget http://files.androtab.info/allwinner/cubieboard2/uImage dd if=u-boot.bin of=/dev/sdb bs=1024 seek=32 mount /dev/sdb1 /mnt/sdcard cp script.bin /mnt/sdcard/ cp uImage /mnt/sdcard/ Note that this kernel embed sw_ahci_platform. In case a SATA hardrive is used, no additional module is needed.\nEnvironment variables uEnv.txt has to be updated (or created) to define the machine ID that the kernel wait for and define the root filesystem. This file is on the first bootable partition (the one where we copied script.bin and uImage).\nmachid=0xf35 root=/dev/mmcblk0p2 Setting MAC address Unlike on cubieboard 1, I did not manage to set the mac address neither from script.bin nor by adding \u0026ldquo;extraargs=mac_addr=\u0026hellip;\u0026rdquo; in uEnv.txt.\nSo I simply created an executable sh file in /etc/network/if-pre-up.d :\n#! /bin/sh ifconfig eth0 hw ether xx:xx:xx:xx:xx:xx MySQL is crashing\u0026hellip; Mysql fail to bind ip address (kernel error ?). As I use it only locally, I forced it to use unix socket instead of local IP (127.0.0.1). The parameter is deprecated but still works for now. In /etc/mysql/my.cnf\nskip-networking ","permalink":"https://www.bluemind.org/linux-hardware-replacing-cubieboard-cubieboard2/","summary":"\u003cp\u003eI recently upgraded one of my cubieboard by a cubieboard 2 (webserver). The goal was to keep everything else but the motherboard\u0026hellip; This post describes main necessary steps to do such a change as quickly and flawlessly as possible (so no custom kernel here\u0026hellip;).\u003c/p\u003e\n\u003cp\u003eThe board I replaced run debian Sid (jessie) on an microSD with kernel 3.0. This microSD is detected as  /dev/sdb on the laptop I use to upgrade.\u003c/p\u003e","title":"Linux / Hardware : replacing cubieboard by cubieboard2"},{"content":"In my cubieboard saga, I continued installing Nginx to both accelerate and securise some Apache hosted sites. Nginx is not always well supported by common opensource web applications such as Wordpress. However, it performs really well as a (cache) reverse proxy. Using Naxsi module you can also build a free and efficient WAF, making Nginx a nice opensource enhencer for Apache hosting.\nThe following describes how to install Nginx with the aforementioned functionalities on a cubieboard (1) running debian with a root filesystem on an sdcard.\nInstall base packages plain and simple : install nginx + naxsi and start nginx at system startup\napt-get install nginx-naxsi update-rc.d nginx defaults Reverse proxy configuration Set main proxy settings\u0026hellip; \u0026hellip;by editing the file /etc/nginx/proxy_params (it is possible to add settings in main nginx.conf, but its less clean IMHO):\n# default nginx header when proxying proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # disable as it is not useful when web server is on another machine proxy_redirect off; # limit max body size. If bigger, throw 413 client_max_body_size 5M; # max buffer size : if content is bigger, it will be written on disk client_body_buffer_size 1M; # bufferize or not response before passing it proxy_buffering on; # set temp path to a tmpfs storage # here on a tmpfs directory for speed # and less sdcard write usage proxy_temp_path /var/tmp/nginx/temp; # activate cache (again, on a tmpfs directory) # levels=1:2 =\u0026gt; set cache structure to [dir]/[file] # keys_zone=static:3m =\u0026gt; set the cache key name and the size for key index # inactive=7d =\u0026gt; delete file older than x days # max_size=200m =\u0026gt; set cache size proxy_cache_key \u0026#34;$scheme://$host$request_uri\u0026#34;; proxy_cache_path /var/tmp/nginx/static levels=1:2 keys_zone=static:3m inactive=7d max_size=200m; Make some adjustments in the main nginx configuration file (/etc/nginx/nginx.conf). Define the number of worker process depending on how many cpu (core) should be used by nginx\nworker_processes 1; # usualy 1 per exposed cpu enable gzip to offload apache server\ngzip on; gzip_disable \u0026#34;msie6\u0026#34;; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_buffers 16 8k; gzip_http_version 1.1; gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript text/x-js; Uncomment naxsi core rules (explained later)\ninclude /etc/nginx/naxsi_core.rules; Include the proxy parameters file\ninclude /etc/nginx/proxy_params; Configure virtual host(s) The following example is based on a Wordpress site with an https encrypted backoffice. As a reverse proxy, Nginx will be used to offload Apache for ssl encryption.\nGenerate a self signed certificate nothing special here :\ncd /etc/ssl openssl genrsa -out private/\u0026lt;domain_name\u0026gt;.key 2048 openssl req -new -key private/\u0026lt;domain_name\u0026gt;.key -out certs/\u0026lt;domain_name\u0026gt;.csr openssl x509 -req -days 365 -in certs/\u0026lt;domain_name\u0026gt;.csr -signkey private/\u0026lt;domain_name\u0026gt;.key -out certs/\u0026lt;domain_name\u0026gt;.crt Create the virtual host file in /etc/nginx/sites-available/\u0026lt;domain_name\u0026gt; Every re-usable generic part is put in external files. This allow to use same configurations for multiple sites. A variable is used to store Ip adress for some external config files.\nserver { listen 80; server_name www.\u0026lt;domain_name\u0026gt; \u0026lt;domain_name\u0026gt;; # custom variable used in conf file set $backend \u0026#34;http://x.x.x.x:80\u0026#34;; include /etc/nginx/custom_conf/base_reverse.conf; include /etc/nginx/custom_conf/reverse_wp.conf; include /etc/nginx/custom_conf/static_files.conf; include /etc/nginx/custom_conf/hardening.conf; include /etc/nginx/custom_conf/blacklist.conf; include /etc/nginx/custom_conf/naxsi_location.conf; # force admin in HTTPS location ~ /wp-(admin|login) { return 301 https://$host$request_uri; } } server { listen 443 ssl; server_name www.\u0026lt;domain_name\u0026gt; \u0026lt;domain_name\u0026gt;; # custom variable used in conf file set $backend \u0026#34;http://x.x.x.x:80\u0026#34;; # configure ssl for admin part ssl_certificate /etc/ssl/certs/www.\u0026lt;domain_name\u0026gt;.crt; ssl_certificate_key /etc/ssl/private/www.\u0026lt;domain_name\u0026gt;.key; keepalive_timeout 70; include /etc/nginx/custom_conf/base_reverse.conf; include /etc/nginx/custom_conf/reverse_nocache_wp.conf; include /etc/nginx/custom_conf/static_files.conf; include /etc/nginx/custom_conf/hardening.conf; include /etc/nginx/custom_conf/blacklist.conf; include /etc/nginx/custom_conf/naxsi_location.conf; } External generic configurations files base_reverse.conf : base configuration.\n# disable access log as apache already store them... # ...and we want minimum write on the sdcard access_log off; #Allow gzip of text based ressources gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript text/x-js application/javascript; reverse_wp.conf : reverse proxy setting for every files (including dynamic php). Files are cached for 1h on the already defined cache entry \u0026ldquo;static\u0026rdquo;. Note the usage of the \u0026ldquo;$backend\u0026rdquo; variable that is set in the virtualhost configuration. The \u0026ldquo;naxsi_wordpress.rules\u0026rdquo; files contains the WAF rules to use (see next chapter)\n# reverse proxy everything + little cache location / { proxy_pass $backend; proxy_cache static; proxy_cache_valid 1h; proxy_cache_use_stale error timeout invalid_header updating; include naxsi_wordpress.rules; } reverse_nocache_wp : reverse proxy setting with no cache (for backoffice)\n# reverse proxy everything location / { proxy_pass $backend; proxy_set_header Host $host; # make apache named virtual host to wor } static_files.conf : reverse proxy optimised for static files (better cache, client rendering optimization\u0026hellip;)\n# specific cache for static files location ~*^.+(swf|jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|js) { proxy_pass $backend; # make apache named virtual host to work proxy_set_header Host $host; # set reverse proxy cache to offload Apache for static files proxy_cache static; proxy_cache_valid 10d; # Allow browser caching for 7 days expires 7d; # authorize cache for browser proxies add_header Pragma public; add_header Cache-Control \u0026#34;public, must-revalidate, proxy-revalidate\u0026#34;; # remove cookies as their are useless for static files fastcgi_hide_header Set-Cookie; # remove php cache control header to control them from nginx fastcgi_hide_header Cache-Control; fastcgi_hide_header Pragma; access_log off; # enable etag for proxy caching optimization on cluster etag on; } hardening.conf: basic security and spam protection hardening\n# Only allow common method : GET, POST and HEAD (for browser caching) if ($request_method !~ ^(GET|HEAD|POST)$ ) { return 444; } # spam protection if ( $http_referer ~* (babes|forsale|girl|jewelry|love|nudit|organic|poker|porn|sex|teen) ) { return 403; } blacklist.conf: black list bad user agent (from HackRepair.com). Lines below are two examples (the real file is way larger)\nif ($http_user_agent ~* \u0026#34;^BlackWidow\u0026#34;){ return 403; } if ($http_user_agent ~ \u0026#34;^Bolt\u0026#34;){ return 403; } naxsi_location.conf: this file contains the location \u0026ldquo;/RequestDenied\u0026rdquo; that is called by naxsi on every forbidden request.\nWaf configuration (naxsi plugin) Default rules are stored in the file \u0026ldquo;naxsi_core.rules\u0026rdquo; which is included in the main config file \u0026ldquo;nginx.conf\u0026rdquo;.\nAny other rule file should be included in virtualhost as rules are usualy specialized by host (different modules, or CMS software).\nTo build a rule file, Naxsi\u0026rsquo;s wiki gives a good base to start with:\n#LearningMode; # uncomment to start learning mode and create a while list SecRulesEnabled; # comment to disabled naxsi (for the location the file is included in) DeniedUrl \u0026#34;/RequestDenied\u0026#34;; # location to go on rejected query ## check rules CheckRule \u0026#34;$SQL \u0026gt;= 8\u0026#34; BLOCK; CheckRule \u0026#34;$RFI \u0026gt;= 8\u0026#34; BLOCK; CheckRule \u0026#34;$TRAVERSAL \u0026gt;= 4\u0026#34; BLOCK; CheckRule \u0026#34;$EVADE \u0026gt;= 4\u0026#34; BLOCK; CheckRule \u0026#34;$XSS \u0026gt;= 8\u0026#34; BLOCK; With only theses rules, there is few chance to get a site working. To customize the rules file, the line \u0026ldquo;LearningMode\u0026rdquo; must be uncomment. This will let pass all queries but log every one which should be blocked.\nSo you can start browsing public and backoffice pages. When its done, a tool named \u0026ldquo;nx_util.py\u0026rdquo; is provided to parse error log file and generate a white list rules.\napt-get install python wget https://naxsi.googlecode.com/files/nx_util-1.0.tgz tar -zxf nx_util-1.0.tgz cd nx_util-1.0/nx_util python nx_util.py -c ./nx_util.conf -l /var/log/nginx/error.log -o Finalize and launch Create cache directories :\nmkdir /var/tmp/nginx/temp mkdir /var/tmp/nginx/static chown -R www-data:www-data /var/tmp/nginx If cache is stored on a tmpfs filesystem (like on example files previsouly presented), theses lines can be added in \u0026ldquo;/etc/rc.local\u0026rdquo; to recreate them on boot.\nEnable site and restart nginx:\ncd /etc/nginx/sites-enabled ln -s ../sites-available/www.\u0026lt;domain_name\u0026gt; /etc/init.d/nginx start Note : if you want apache to log the real IP address of the visitor and not the nginx one, use mod_rpaf(package libapache2-mod-rpaf)\n","permalink":"https://www.bluemind.org/linux-nginx-waf-reverse-proxy-for-wordpress-running-apache/","summary":"\u003cp\u003eIn my cubieboard saga, I continued installing Nginx to both accelerate and securise some Apache hosted sites.\nNginx is not always well supported by common  opensource web applications such as Wordpress. However, it performs really well as a (cache) reverse proxy. Using Naxsi module you can also build a free and efficient WAF, making Nginx a nice opensource enhencer for Apache hosting.\u003c/p\u003e\n\u003cp\u003eThe following describes how to install Nginx with the aforementioned functionalities on a cubieboard (1) running debian with a root filesystem on an sdcard.\u003c/p\u003e","title":"Linux: Nginx as a WAF and reverse proxy (for Wordpress running with Apache on cubieboard)"},{"content":"I was searching how to replace my X86 server by some arm based server. The main objectives was to lesser power comsumption and optimize the fault tolerance of m infrastructure (several am computer is better than one simple x86 machine running virtual machines\u0026hellip;).\nAfter a little research on arm computer, I chose the Cubieboard for its good power/price ratio and the support we can find for it.\nBasicaly, I found all informations I needed on the following site : https://github.com/linux-sunxi/u-boot-sunxi/wiki\nSDCard preparation The hardware I used a SanDisk microSDHC \u0026ldquo;ultra 10x\u0026rdquo; as main drive to insure good speed (the cubieboard\u0026rsquo;s internal nand flash seems a little bit slow compared to this microSD).\nOptimization Thanks to sdcard optimization (see article), I got the following results:\nbig files before optimization : 821 Mb/s, after : 980 Mb/s small files : Before optimizations : 270 Mb/s, after 618 Mb/s Partitioning I organized the sdcard as follow :\na first partition for /boot of 28 Mb: from block 8192 to 65535 (fat fs, as the cubiboard only support fat for booting) a second part for / starting at block 65536 to the end (ext4 fs) Deploying the base system The way I chose to Install Debian on my cubieboard, I chose to get an image of a base install and then copy files to the final destination SdCard (by default, the cubieboard tries to boot from SDcard slot first).\nI got the base debian wheezy image from : http://guillaumeplayground.net/share/debian_wheezy_armhf_v1_mele.img.gz\nThen I wrote this image on a usb stick (need to be \u0026gt;=1Gb):\ndd if=debian_wheezy_armhf_v1_mele.img of=/dev/sdc bs=1024 Finaly I used rsync to copy the content of the 2 partitions form the usb stick to the 2 partitions of my SdCard (first part = boot, second part = root)\nrsync -avc /source /target Make the SdCard bootable To allow the Cubieboard to boot on a microSD, we need to copy the u-boot bootloader on the very first blocks. In order to do that we have to compile u-boot for this specific board (I\u0026rsquo;m using archlinux on my laptop and /dev/sdc is the microSd) :\nyaourt -S arm-none-linux-gnueabi git clone git://github.com/linux-sunxi/u-boot-sunxi.git cd u-boot-sunxi make \u0026#39;cubieboard\u0026#39; CROSS_COMPILE=arm-none-eabi- dd if=spl/sunxi-spl.bin of=/dev/sdc bs=1024 seek=8 dd if=u-boot.bin of=/dev/sdc bs=1024 seek=32 Then put the card in the cubieboard, And was able to ssh to it after a little research for the ip it had been given by my DHCP (l/p = root/root)\nFinalize the system Basic Customization passwd echo \u0026#34;\u0026lt;MY_HOSTNAME\u0026gt;\u0026#34; \u0026gt; /etc/hostname Optimizing: I updated the whole sytem thought apt-get dist-upgrade, then like for my Sheevaplug I made some tmpfs mountpoint in /etc/fstab\nI also set the IO Scheduler to Deadline (better than \u0026ldquo;noop\u0026rdquo; for nand) and overclocked a little bit the SoC in rc.local:\ndeadline scheduler cpufreq-set -u 1.2GHz Make use of the SATA Port As I want to use my Cubieboard as a personnal web server with lots of images and/or videos, I needed to use an hardrive in addition to the SdCard.\nThe Cubieboard provide a Sata Port and suitable cable for a 2.5 inch drive, but the port is not powered by default. To enable this, you have to modify the boot params of the bootloader (the .fex file in /boot):\napt-get install g++ git clone git://github.com/linux-sunxi/sunxi-tools cd sunxi-tools make mount /boot ./bin2fex /boot/script.bin /tmp/script.fex nano /tmp/script.fex # set sata_power_en = port:PB08\u0026lt;1\u0026gt;\u0026lt;default\u0026gt;\u0026lt;default\u0026gt;\u0026lt;0\u0026gt; ./fex2bin /tmp/script.fex /boot/script.bin umount /boot echo \u0026#34;sw_ahci_platform\u0026#34; \u0026gt;\u0026gt; /etc/modules # load sata module at boot Note that depending on the power your hardrive need, you may have to plug another power supply throught the mini-usb (OTA). In my Case, I had to add an 1A powersupply together with the original one\u0026hellip;\nAfter a reboot the hardrive powers on and is recognized by the system. Installating the Lamp stack is then pretty straight forward, like on any debian server\u0026hellip;\n","permalink":"https://www.bluemind.org/linux-cubieboard-webserver/","summary":"\u003cp\u003eI was searching how to replace \u003ca href=\"/linux-openvz-archlinux/\" title=\"Linux : my experience about OpenVZ and Archlinux\"\u003emy X86 server\u003c/a\u003e by some arm based server.  The main objectives was to lesser power comsumption and optimize the fault tolerance of m infrastructure (several am computer is better than one simple x86 machine running virtual machines\u0026hellip;).\u003c/p\u003e\n\u003cp\u003eAfter a \u003ca href=\"/hardware-arm-mini-pc-replace-x86-server/\" title=\"Hardware : which ARM mini-pc to replace an x86 server ?\"\u003elittle research on arm computer\u003c/a\u003e, I chose the Cubieboard for its good power/price ratio and the support we can find for it.\u003c/p\u003e","title":"Linux / Hardware : Using a Cubieboard as (web)server"},{"content":"After having done a little research on arm based computer (see article), I found an used Sheevaplug for less than 40 euros (!)\u0026hellip; So I decided to use it as a NAS. Below is the main steps I followed to install it : easier than installing a classical x86 debian.\nLike I did on my previous x86 server, I use two 2To sata disks : one for main storage plugged on the eSata port, and another plugged on the usb2 port for mirroring (sync\u0026rsquo;ed every night with rsync then turned off for power saving and disk protection)\nPreparing The following site contains a simple and well made guide to get a debian Squeeze install image for the kirkwood architecture (the one used by the Sheevaplug): http://www.cyrius.com/debian/kirkwood/sheevaplug/install.html\nAs I wanted to optimize SdCard partition (see article), I prepared the sdcard on my desktop (archlinux). After partitioning, I made a LVM partition the classical way before formating in ext4:\npvcreate /dev/sdb1 vgcreate vg-data /dev/sdb1 lvcreate -n lv-data -L 7.27g vg-data As I wanted to install the root partition on the SdCard, I could not use it as install media. So I put the install image (uImage) and the initrd image (uInitrd) on an usb pendrive (/boot will be on internal sheevaplug\u0026rsquo;s flash: /dev/mmcblk0p1).\nBoot sheevaplug and follow the installer I followed the aforementioned guide and I made the following choices during the debian install process:\ndo manual partition choose not to format existing partition (just select mount point as I previously optimised and formated the SdCard) ignore error about bad option on file system (due to no journaling) =\u0026gt; answer no answer no for swap too (not need for swap in my case) The base system install begin, and all is automated\u0026hellip;\nOptimize system for flash drive (sdcard) In order to optimize the whole system for flash drive (more speed and less write access) I made some custom changes to both /etc/fstab and /etc/rc.local.\nOn fstab I mainly added tmpfs partitions for frequently written directories as well as more secure mount options:\ntmpfs /tmp tmpfs nodev,nosuid,noatime 0 0 tmpfs /var/tmp tmpfs nodev,nosuid,noatime 0 0 tmpfs /var/log tmpfs nodev,nosuid,noatime,size=20M 0 0 tmpfs /var/backups tmpfs nodev,nosuid,noatime,size=10M 0 0 tmpfs /var/run tmpfs defaults,noatime,size=1M 0 0 tmpfs /var/lock tmpfs defaults,noatime,size=1M 0 0 Then in /etc/rc.local I setup \u0026ldquo;deadline\u0026rdquo; scheduler which is better than the default one which is made for harddrive. Note That I don\u0026rsquo;t use the \u0026ldquo;noop\u0026rdquo; scheduler because \u0026ldquo;deadline\u0026rdquo; can be better as it group small accesses, which improve latency.\necho deadline \u0026gt; /sys/block/mmcblk0/queue/scheduler Finaly I made a little script based on my own shell library to sync /var/log and /var/backup to a non tmpfs partition in order to keep logs on a non-volatile support. I run this on a weekly basis throught /etc/cron.weekly. This mecanism is safe enought for me as my sheevaplug is powered from a battery backed up source, so there is very few chance to loose log files (ok, the server could crash and I accept that)\u0026hellip;\n#!/bin/bash . /etc/bluemind/functions synclog syncbak cleanTmp NAS softwares After having finished the base system install and optimizations, I configured two main software for a NAS : Network Sharing and search engine.\nSamba install There is not a lot to say for samba installation as it is pretty straight forward using \u0026ldquo;apt-get\u0026rdquo;. I got the best speed results (thought both wired and wireless connections) with the following adjustments in the configuration file (/etc/samba/smb.conf)\nsocket options = SO_KEEPALIVE TCP_NODELAY IPTOS_LOWDELAY SO_RCVBUF=65536 SO_SNDBUF=65536 read raw = Yes write raw = Yes getwd cache = Yes A search engine for hosted document I used \u0026ldquo;regain\u0026rdquo; to index all my documents. It works more or less like google and is very usefull to find documents on the NAS (including mp3, as it indexes ID3 tags).\nI got the server version on sourceforge and unzipped it in /opt. Then, I followed the install instructions on http://regain.murfman.de/wiki/doku.php?id=installation:server (I chose to install it on /opt)\nTo run the web UI you need to install tomcat\napt-get install tomcat6 Then change the memory allocated to the tomcat\u0026rsquo;s JVM (32M is enought for regain web interface) in /etc/default/tomcat6 :\nJAVA_OPTS=\u0026#34;-Djava.awt.headless=true -Xmx32m -Xms32m -XX:+UseConcMarkSweepGC\u0026#34; Finaly I made a script to update document index every week if any new file has been added on the NAS during the last 7 days (again, based on my own shell framework):\n#!/bin/bash . /etc/bluemind/functions headerMessage \u0026#34;Documents indexing for $HOSTNAME\u0026#34; # get number of modified file nbfile=$(getNbChangedFile \u0026#34;/path/1 /path/2 /path/X\u0026#34; -7) if [ $nbfile -eq \u0026#34;0\u0026#34; ]; then footerMessage exitOK \u0026#34;No change to document =\u0026gt; nothing new to index\u0026#34; else indexDocuments exitOK \u0026#34;Documents indexing output ($nbfile changes)\u0026#34; footerMessage fi ","permalink":"https://www.bluemind.org/linux-sheevaplug-perfect-nas/","summary":"\u003cp\u003eAfter having done a little research on arm based computer (\u003ca href=\"/hardware-arm-mini-pc-replace-x86-server/\" title=\"Hardware : which ARM mini-pc to replace an x86 server ?\"\u003esee article\u003c/a\u003e), I found an used Sheevaplug for less than 40 euros (!)\u0026hellip; So I decided to use it as a NAS. Below is the main steps I followed to install it : easier than installing a classical x86 debian.\u003c/p\u003e\n\u003cp\u003eLike I did on \u003ca href=\"/linux-openvz-archlinux/\" title=\"Linux : my experience about OpenVZ and Archlinux\"\u003emy previous x86 server\u003c/a\u003e, I use two 2To sata disks : one for main storage plugged on the eSata port, and another plugged on the usb2 port for mirroring (sync\u0026rsquo;ed every night with rsync then turned off for power saving and disk protection)\u003c/p\u003e","title":"Linux : Sheevaplug as a perfect NAS"},{"content":"As informations are scattered on severals forum threads and various websites, I made a little summary of ressources and steps to follow to install CynogenMod 10.1 on an LG Optimus 4x HD (implies unlocking the bootloader, thus warranty lost)\u0026hellip; it might help someone ;)\nNote : I did not manage to make the following to works with a (VirtualBox) Win XP Virtual machine (thought linux host) as the only usb device that can be added to the VM is \u0026ldquo;LG Modem\u0026rdquo; and the upgrade software need a direct ADB connection. There may be a solution by changing some udev rules, but as I had a windows machine in side of me, I didn\u0026rsquo;t search more than a few minutes\u0026hellip;\nStep 1 : install the unlockable V20A_00 firmware Install LGUnitedMobileDriver_S4981MAN38AP22_ML_WHQL_Ver_3.8.1.exe Install Fastboot driver (CT_HsPhone_General_Drivers.rar) 64/DPInst.exe plug phone and check that windows recognize the device (activate debug on phone) unactivate Modems/LGE AndroidNet USB Modem in peripherals manager boot in download mode (usual procedure: remove battery, wait 30 sec, keep vol down pushed and plug usb, then put the battery back) wait for device to be detected by Windows then launch KDZ_FW_UPD in administrator check that \u0026ldquo;3GQCT\u0026rdquo; and \u0026ldquo;DIAG\u0026rdquo; are selected in comboboxes, select the KDZ file (V20A) then clic \u0026ldquo;Launch software update\u0026rdquo; wait for the process to finish (the phone should reboot) if bootloop : boot recovery (vol down + power), then factory reset and wipe cache) Be aware that some firmware image are no unlockable\u0026hellip; I got 2 the one which worked has a size of 602 Mb Step 2 : unlock bootloader + install CWM use all-in-one script (unplug usb, activate debug mode then plug usb back) first question : step 2 (driver already installed in previous step) option 2 : unlock bootloader (phone should reboot ask you weither your are shure : if yes push vol up. When done, remove cable and battery, then put them back to reboot) option 1 : root (follow on screen procedure : remove usb cable, reboot recovery, select ADB update and press any key, then reboot and check that you have a new app : su) Install Rom Manager from google play and install CWM (6.0.3 minimum required for CM 10.1) Note: I don\u0026rsquo;t use the option 6 of the script \u0026ldquo;all-in-one\u0026rdquo; as it installs CWM 6.0.1, not 6.0.3) unplug usb cable, disable usb debuging, replug usb cable and select MTP copy any p880 nightly build to external sdcard boot recovery, wipe all (factory + dalvik) install from zip (external sdcard) then reboot Step 4 : additional mods / apps (personal taste) Bravia_Engine_2 : install zip and modify build.prop according to the post google apps (see the CM\u0026rsquo;s wiki) V6Supercharger : SdCard Speed Tweaks, 3G TurboCharger, Flush-O-Matic, Fix Emissions, Wheel alignment, Detailling (clean database) on boot Set density to 280 (/system/build.prop : ro.sf.lcf_density\u0026hellip;) To come : replace the kernel (I\u0026rsquo;m thinking about WerewolfJB kernel) Links All-In-One Script : http://forum.xda-developers.com/showthread.php?t=2230934 CT_HsPhone_General_Drivers : http://forum.xda-developers.com/showthread.php?t=2180497\u0026amp;page=2 (Scroll down) LGUnitedMobileDriver : http://www.lg.com/sg/support-mobile/lg-Optimus-4X-HD-P880 UpgradeKDZ : http://androidromupdate.com/2013/04/14/manual-upgrade-lg-optimus-4x-hd-p880-to-v20a-official-android-4-1-2-jelly-bean/ Bravia_Engine2: http://forum.xda-developers.com/showthread.php?t=2223835 LGNotification : http://forum.xda-developers.com/showthread.php?t=1821249 ","permalink":"https://www.bluemind.org/android-install-cm-10-1-unlocked-rooted-lg-optimus-4x-hd/","summary":"\u003cp\u003eAs informations are scattered on severals forum threads and various websites, I made a little summary of ressources and steps to follow to install CynogenMod 10.1 on an LG Optimus 4x HD (implies unlocking the bootloader, thus warranty lost)\u0026hellip; it might help someone ;)\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eNote : I did not manage to make the following to works with a (VirtualBox) Win XP Virtual machine (thought linux host) as the only usb device that can be added to the VM is \u0026ldquo;LG Modem\u0026rdquo; and the upgrade software need a direct ADB connection.\u003c/em\u003e \u003cem\u003eThere may be a solution by changing some udev rules, but as I had a windows machine in side of me, I didn\u0026rsquo;t search more than a few minutes\u0026hellip;\u003c/em\u003e\u003c/p\u003e","title":"Install CyanogenMod 10.1 on an LG Optimus 4X HD"},{"content":"As a lazy sysadmin, I try to make my servers as autonomous as possible : auto-backup, auto-sync tmpfs, report disk status by email, etc.\nFor all of that, I use a custom little shell framework and some scripts that I share with all my servers through my own git repository.\nI decided to publish theses files : it might be useful for some one ;). The \u0026ldquo;framework\u0026rdquo; and configuration variables are in bluemind_shellFramework.zip whereas scripts are in bluemind_shellScripts.zip.\nWhat the framework provides Description Basicaly, the framework has the following features :\nRedirect all output to a formated log file (eg: prefix added to every line) Log file send to admin Error manager (auto umounts, send mail, etc.) Provides base functionalities such as directory sync, log writing, mysql backup, directory backup, changes detection in directory, etc. Output example The framework allows me to write small scripts that output log file like the following (for instance, a backup script):\n[17.04.2013-03:00:03][filesrv] ############################## [17.04.2013-03:00:03][filesrv] Home mirroring for filesrv [17.04.2013-03:00:03][filesrv] ############################## [17.04.2013-03:00:03][filesrv] [17.04.2013-03:00:03][filesrv] Mount mirror partition [17.04.2013-03:00:03][filesrv] Start Syncronization... [17.04.2013-03:00:03][filesrv] [17.04.2013-03:00:03][filesrv] building file list ... done [17.04.2013-03:00:03][filesrv] home/hidden/path/to/some/changed/file [17.04.2013-03:00:03][filesrv] home/hidden/path/to/some/changed/file/again [17.04.2013-03:00:03][filesrv] [17.04.2013-03:00:03][filesrv] sent 2399992 bytes received 34 bytes 62338.34 bytes/sec [17.04.2013-03:00:03][filesrv] total size is 925955506298 speedup is 385810.61 [17.04.2013-03:00:48][filesrv] [17.04.2013-03:00:48][filesrv] Unount mirror partition [17.04.2013-03:00:49][filesrv] [17.04.2013-03:00:49][filesrv] ############################## [17.04.2013-03:00:49][filesrv] Example scripts To give some ideas of the framework usage, bellow is 2 examples of scripts I use (together with cron).\nSync logs and backup files from tmps mountpoint to a static storage, plain, simple:\n#/bin/bash . /etc/bluemind/functions synclog syncbak cleanTmp Another one to backup whole file system and mysql databases with automatic detection of dedicated mount points for /boot and/or /var :\n#!/bin/bash . /etc/bluemind/functions headerMessage \u0026#34;Starting backup of $HOSTNAME\u0026#34; apt-get clean # set BACKUP_DIR to local if needed if ! [ -d $BACKUP_DIR ] ; then message \u0026#34;no remote backup dir found : using local backup dir\u0026#34; BACKUP_DIR=\u0026#34;$BACKUP_LOCAL_DIR\u0026#34; else message \u0026#34;mounting storage disk\u0026#34; mountBackupDir fi message \u0026#34;Starting backup of /\u0026#34; backup \u0026#34;/\u0026#34; $BACKUP_DIR \u0026#34;$BACKUP_FILENAME.ROOT\u0026#34; # make a separate backup if /boot is a partition if [ \u0026#34;`grep /boot /etc/fstab`\u0026#34; ] ; then message \u0026#34;Starting backup of /boot\u0026#34;; mountBoot backup \u0026#34;/boot\u0026#34; $BACKUP_DIR \u0026#34;$BACKUP_FILENAME.BOOT\u0026#34; fi # make a separate backup if /var is a partition if [ \u0026#34;`grep /var /etc/fstab | grep ext4`\u0026#34; ] ; then message \u0026#34;Starting backup of /var\u0026#34;; backup \u0026#34;/var\u0026#34; $BACKUP_DIR \u0026#34;$BACKUP_FILENAME.VAR\u0026#34; fi # mysql database backup if [ -x mysqld ] ; then message \u0026#34;MySQL detected !\u0026#34; backupMysql $BACKUP_DIR \u0026#34;$BACKUP_FILENAME.sql\u0026#34; fi message \u0026#34;Unount storage and snapshot\u0026#34; umountDirs footerMessage exitOK \u0026#34;backup\u0026#34; ","permalink":"https://www.bluemind.org/linux-custom-shell-framework-scripts-servers/","summary":"\u003cp\u003eAs a lazy sysadmin, I try to make my servers as autonomous as possible : auto-backup, auto-sync tmpfs, report disk status by email, etc.\u003c/p\u003e\n\u003cp\u003eFor all of that, I use a custom little shell framework and some scripts that I share with all my servers through my own git repository.\u003c/p\u003e\n\u003cp\u003eI decided to publish theses files : it might be useful for some one ;).  The \u0026ldquo;framework\u0026rdquo; and configuration variables are in bluemind_shellFramework.zip whereas scripts are in bluemind_shellScripts.zip.\u003c/p\u003e","title":"Linux : custom shell framework and scripts for my servers"},{"content":"I recently created a new NAS server using a sheevaplug (article to come\u0026hellip;) and a 8Gb Sdcard for the system partition.\nMaybe you know that partitioning and formatting sdcard should be done carefully to get best speed and lifetime. This require to respect the both erasure block and segment size : this is exactly what I made and what is this article about.\nI used the \u0026ldquo;flashbench tool\u0026rdquo; from : https://github.com/bradfa/flashbench.\nUsage ./flashbench -a /dev/mmcblk0 --blocksize=1024 Note that I sometime need top launch it several times to get consistent values (eg: non negative time). Based on the flashbench author\u0026rsquo;s explanation, I could find:\nerasure block : biggest value for which the time become near the double of the previous one. In my case it was 4Mb segment size (block size) : smallest value for which the time become significantly higher than the previous one. For me : 4k Then, simply calculate all the needed informations to partition with fdisk and format with ext4. I made an M$ Excel sheet to define parameters for every SDCard I use (file is attached to this post)\u0026hellip; Note that I commented out the file for easier usage\u0026hellip; ;)\nSmall benchmarks For Bigfiles : sync; rm bigfile; sync; time ( dd if=/dev/zero of=bigfile bs=16k count=10000; sync) ; rm bigfile Before optimization I got : 18.2 and after I had 21.9. Ok, that\u0026rsquo;s not so much, but look further for small files\u0026hellip;\nFor small files: sync; rm -rf smallfiles_*; sync; time ( for i in `seq 1 100`; do dd if=/dev/zero of=smallfiles_$i bs=16k count=10; sync; done; ) ; rm -rf smallfiles_* Before optimization, I got 148 Mb/s and after : 184 Mb/s !\n","permalink":"https://www.bluemind.org/hardware-linux-optimize-sdcard-nand-flash-speed-lifetime/","summary":"\u003cp\u003eI recently created a new NAS server using a sheevaplug (article to come\u0026hellip;) and a 8Gb Sdcard for the system partition.\u003c/p\u003e\n\u003cp\u003eMaybe you know that partitioning and formatting sdcard should be done carefully to get best speed and lifetime. This require to respect the both erasure block and segment size : this is exactly what I made and what is this article about.\u003c/p\u003e\n\u003cp\u003eI used the \u0026ldquo;flashbench tool\u0026rdquo; from : https://github.com/bradfa/flashbench.\u003c/p\u003e","title":"Hardware / Linux : optimize SdCard (nand flash) speed and lifetime  "},{"content":"I actually have an x86 server (based on an Intel Atom N330) that consumes around 40W. As ARM based mini-pc offer is growing, I selected some devices that are eligible to replace my x86 server.\nI\u0026rsquo;m not going to replace my actual server with only one Arm mini-pc : I\u0026rsquo;m more on replacing each OpenVZ container I use by an Arm device. Even with 3 Arm mini-pcs, I will devide by 2 or more the power consumption with at least the same computing power (if not more).\nBellow is a comparison sheet I made, based on my following ideas and expectations:\nI don\u0026rsquo;t look anything with less than 512 Mb RAM (think about flash storage optimization and native linux file cache mecanism) I don\u0026rsquo;t look anything without an ethernet port (I don\u0026rsquo;t like wifi for servers, nor usb2ethernet) I tried to estimate the approximate computing power to compare against well known x86 processors (but I know that it really depends on real usage\u0026hellip;) A NAS can only be done with a gigabit ethernet and sata/e-sata port (full hd mkv streaming, big files transfert throught dual channel wifi, etc.) Final price should be estimated with delivery cost, local taxes and money (France, Euro) Linux support (at least debian or archlinux) Name Raspberry Pi model B Hackberry CuBox Ordroid U2 sheevaplug CPU Arm11 @700 Mhz (Armv6) Arm Cortex A8 @1200Mhz (Arm v7) Arm PJ4 @800 Mhz (Arm v7) Quad core Arm Cortex-A9 @1700 Mhz Arm9e @1200Mhz (Arm v5) Speed compare to Pentium 2 300Mhz / Via C7 Atom n230 ? core 2 duo @1200Mhz Pentium 2 300Mhz / Via C7 Ram 512 Mb 1 Gb 1 Gb 2 Gb 512 Mb Network 10/100 Mbps 10/100 Mbps + Wifi n 10/100/1000 Mbps 10/100 Mbps 10/100/1000 Mbps USB 2 2 0 2 2 1 SD / MicroSD Sdcard Sdcard MicroSD MicroSD Sdcard (Micro) HDMI Yes Yes Yes Yes No Audio (RCA/Jack) Yes Yes No Yes No eSata / Sata No No Yes No Yes Others RCA for video Composite, Serial Spdif, microUsb microUsb microUsb Supported OS Debian, archlinux, RiscOS Debian, Android Debian, archlinux, ubuntu, Android Android, Ubuntu, Fedora, community Debian Ubuntu, Debian, Archlinux Idea of usage OpenVPN, Domotic, small web server Webserver, domotic, OpenVPN Nas with additional services (search, upnp, etc.) Strong Webserver Nas with simple additional services (upnp, etc.), domotic Where to Buy http://www.kubii.fr https://www.miniand.com http://www.solid-run.com/ http://www.hardkernel.com https://www.globalscaletechnologies.com Original price 37.99€ $65 $119 $89 $99 Full price with Case and power supply 56.06€ No case $119 $98 $99 Final Full Price 64.71€ 99€ 174€ 149€ 144€ Here come 2 new challengers (edited on 08.03.2013):\nName cubieboard pandaboard CPU Arm Cortex A8 @1000Mhz (Arm v7) Dual Arm Cortex A9 @1200Mhz (omap 4) Speed compare to Atom n230 little less than atom z530 Ram 1 Gb 1 Gb Network 10/100 Mbps 10/100 Mbps USB 2 2 2 SD / MicroSD MicroSd Sdcard (Micro) HDMI Yes Yes Audio (RCA/Jack) Yes Yes eSata / Sata Yes No Others microUsb, 1 IR microUsb Supported OS Ubuntu, debian, android Ubuntu, debian, android Idea of usage webserver, domoticr good webserver Where to Buy http://www.aliexpress.com http://tigal.com Original price $49.9 149€ Full price with Case and power supply $72.9 200€ Final Full Price 85€ 211€ ","permalink":"https://www.bluemind.org/hardware-arm-mini-pc-replace-x86-server/","summary":"\u003cp\u003eI actually have an x86 server (based on an Intel Atom N330) that consumes around 40W. As ARM based mini-pc offer is growing, I selected some devices that are eligible to replace my x86 server.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;m not going to replace my actual server with only one Arm mini-pc : I\u0026rsquo;m more on replacing each OpenVZ container I use by an Arm device. Even with 3 Arm mini-pcs, I will devide by 2 or more the power consumption with at least the same computing power (if not more).\u003c/p\u003e","title":"Hardware : which ARM mini-pc to replace an x86 server ?"},{"content":"Some time ago (a year or so), I changed the hardware of my own server. I decided to take the opportuny to change my VM system from XEN 3 to OpenVZ (for a lots of reasons that are not in the scope of this article ;).\nI finaly managed to take some time to write something about this story on my blog. This is far from being a guide : it\u0026rsquo;s more a suite of notes\u0026hellip;\nChosen Hardware and linux distro As I\u0026rsquo;m a big fan of Archlinux, this is the distro I choosed (simplicity, stability, up to date, \u0026hellip;)\nFor the hardware, as my server is running 24/7, I wanted a quite \u0026ldquo;green\u0026rdquo; solution ;). I opted for an Atom based architecture which is powerfull enought for my own usage (3 containers) and as a limited power consumption:\nMotherboard : Asrock A330 GC including an atom 330 at 1.6Ghz (overclocked to 1.8 ghz) Memory : 4 Gb of RAM Main system disk : 8Gb compaq flash mounted on an IDE adapter (more than 40 Mb/s for a very light power usage) VM disks : 2.5 inch 40 Gb drive for containers (VM) and logs partition, two 3.5 inch, 5400 rpm, 2Tb for data storage and backup (soft mirrored: I hate raid as a faulty controller could make 2 unusable disks\u0026hellip;) Dual gigabit network cards (bonded and dispached using software bridge between containers) Basic container (VM) Creation Partitions on OpenVZ Host The host of all container use the Compaq flash for the main system disk with 3 partitions : /, /boot and /var. The 2.5 inch 40Gb drive as one partition dedicated to /var/log (to avoid premature death of the compaq flash) and the rest of the disk is used as LMV volume for containers.\nThis solution as severals advantages :\nThe compaq flash insure best file access for the base system (with is more important than brute speed for the container host) LVM for containers offer the best option (IMHO) for re-sizing and snapshot-ting containers (think about backup) Below is the fstab file of my container host:\n# tmpfs for misc directories that often operate best in memory instead of disk tmpfs /tmp tmpfs nodev,nosuid 0 0 tmpfs /var/run tmpfs defaults,noexec,noatime,size=2M 0 0 tmpfs /var/lock tmpfs defaults,noexec,noatime,size=2M 0 0 # here are the compaq flash partitions. Note flash drive optimised parameters LABEL=BOOT /boot ext2 defaults,noauto,noatime 0 1 LABEL=ROOT / ext4 defaults,noatime,async,barrier=0,commit=100 0 1 LABEL=VAR /var ext4 defaults,noatime,async,barrier=0,commit=100 0 1 ########### OpenVZ dedicated partition ########## # container disks (replaced with dummy names for this article) /dev/OZDisks/xxx /vz/private/xxx ext4 defaults,noatime,async 0 1 /dev/OZDisks/yyy /vz/private/yyy ext4 defaults,noatime,async 0 1 /dev/OZDisks/zzz /vz/private/zzz ext4 defaults,noatime,async 0 1 Creating a template container In order to deploy several containers with the same base configuration I first create a template container:\nwget http://download.openvz.org/template/precreated/contrib/arch-2010.05-x86_64-minimal.tar.gz vzctl create 99 --ostemplate arch-2010.05-x86_64-minimal --private=/vz/private/TmplArchLinux/99 vzctl set 99 --userpasswd root: vzctl set 99 --diskspace 5G:5G --save vzctl set 99 --privvmpages 256M:256M --save vzctl start 99 vzctl enter 99 Then, when the container is started, I just need to ajust the hostname in /etc/rc.conf\nNetwork settings Bonding configuration on OpenVZ host and containers To enabled bonding and vzbridge (for container disptching) on archlinux I had to comment out all network settings on /etc/rc.conf and set NETWORKS Variable like the following :\nNETWORKS=(bonded vzbridge) This force the loading of corresponding files from /etc/network.d at boot.\nBonded file is pretty standard (note the IP=\u0026ldquo;0\u0026rdquo; : ip address is set up at bridge level, in the bridge file):\nCONNECTION=\u0026#34;bonding\u0026#34; INTERFACE=\u0026#34;bond0\u0026#34; SLAVES=\u0026#34;eth0 eth1\u0026#34; IP=\u0026#34;0\u0026#34; DHCP_TIMEOUT=40 For bridge setting, you first need to install bridge utils (pacman -S bridge-utils). Then the config file is :\nINTERFACE=\u0026#34;br0\u0026#34; CONNECTION=\u0026#34;bridge\u0026#34; DESCRIPTION=\u0026#34;VZ Bridge for VM\u0026#34; BRIDGE_INTERFACES=\u0026#34;bond0\u0026#34; IP=\u0026#34;dhcp\u0026#34; DHCP_TIMEOUT=40 At this time, the host server as a network access using DHCP with a bonded interface (2x1Gbps here).\nUse bonded interface in VM + DHCP First, you need to create the file /etc/vz/vznet.conf in order to make openvz to configure veth network using bidge-utils for containers (vm). This file just define the path to a script that come with openvz (don\u0026rsquo;t forget to set the executable bit on it):\n#!/bin/bash EXTERNAL_SCRIPT=\u0026#34;/usr/sbin/vznetaddbr\u0026#34; Then we juste need to add a network interface for each container. Example for a container with ID 99 (you can set the mac adress as you wish):\nvzctl set 99 --netif_add eth0,00:00:00:00:00:99,,,br0 --save We are almost done. We need to configure network on the container\u0026hellip; but for dhcp to work we have to comment the 2 lines In the archlinux tempate setting that begin with ADD_IP and DEL_IP (/etc/vz/dist/arch.conf).\nFor final touch, on the container rc.conf file, ajust settings as the following:\nset eth0=\u0026ldquo;dhcp\u0026rdquo; check that \u0026ldquo;eth0\u0026rdquo; is defined in the INTERFACES variable check that lo=\u0026ldquo;lo 127.0.0.1\u0026rdquo; is declared check that ROUTES=(!gateway) After running /etc/rc.d/network restart, the container should have an IP address from the dhcp server, and an access to the internet\u0026hellip;. if the host has it ;)\nResources allocations for containers Summary of OpenVZ ram parameters Mistakes in any of openvz memory parameter can lead to bad performance and/or unexpected process kill. If I had to remember something I would say :\nPrivvmempages : allocable memory (allocate != used). Barrier = limit, whereas limit is not used vmguardpages : garanteed memory, even if host is out of memory (and so swap begins to be used) oomguardpages: threshold before any process kill kmemsize = non swappable kernel memory used for processes (check usage thought userbean counter) To make things clear, a process kill will occur when : limit vmguardpages \u0026lt; oomguardpages + socket buffer +kmemsize\nAdding peripherals This is pretty simple : we just use the DEVNODES variable. For example adding an harddrive :\nEVNODES=\u0026#34;sdc1:rw\u0026#34; Allowing HTTPS/SSL and SSHD usage on a container By default, containers don\u0026rsquo;t have \u0026ldquo;random\u0026rdquo; device needed for SSL to generate key (they need some entropy found in random devices). As OpenVZ is not compatible with udev, special devices must be created manualy.\nSo, to make apache works with HTTPS, we need to add /dev/random and /dev/urandom to the container (here, ID 99) :\nvzctl exec 99 rm /dev/urandom vzctl exec 99 rm /dev/random vzctl exec 99 mknod /dev/random c 1 8 vzctl exec 444 mknod /dev/urandom c 1 9 Then, on the container configuration file, we need to add the urandom device:\nDEVICES=\u0026#34;c:1:9:rw \u0026#34; Allowing OpenVPN on a container Yes it is possible to use a container as an OpenVPN server\u0026hellip; this is one of the advantages OpenVZ has over other containers system (eg: lxc).\nLike for SSL, we need to add some devices (here with tun. It should be similar for tap):\nvzctl set 101 --devices c:10:200:rw --save vzctl exec 101 mkdir -p /dev/net vzctl exec 101 mknod /dev/net/tun c 10 200 vzctl exec 101 chmod 600 /dev/net/tun Then to allow the container to use iptables (need for openVPN routing rules), we need to set the following on the container config file :\nCAPABILITY=\u0026#34;NET_ADMIN:on \u0026#34; Finally, when OpenVPN is installed on the container, we need to add a routing rule to make the container act as a router to the lan for any connected client.\nIn the following example, the vpn network is 192.168.100.0 and the IP of the container is 192.168.1.3:\niptables -t nat -A POSTROUTING -s 192.168.100.0/24 -j SNAT --to-source 192.168.1.3 That\u0026rsquo;s all folks ! I don\u0026rsquo;t have anything else to write. Everything is working very well since more than one year. This allow to have multiple virtual server with a minimum overhead on the server (remember that atom n330 has vm extension)\nI will try to publish some of the script I used for maintenance (automatic backup of all container, automatic ssh ip ban, etc.).\nThe only thing which is sad is that upgrading Archlinux can be tricky sometime as the container does not run the initial init scripts of the distribution\u0026hellip;\n","permalink":"https://www.bluemind.org/linux-openvz-archlinux/","summary":"\u003cp\u003eSome time ago (a year or so), I changed the hardware of my own server. I decided to take the opportuny to change my VM system  from XEN 3 to OpenVZ (for a lots of reasons that are not in the scope of this article ;).\u003c/p\u003e\n\u003cp\u003eI finaly managed to take some time to write something about this story on my blog. This is far from being a guide : it\u0026rsquo;s more a suite of notes\u0026hellip;\u003c/p\u003e","title":"Linux : my experience about OpenVZ and Archlinux"},{"content":"You may have encoutered, like me, some problem with LAN access while using the Cisco VPN Client.\nThis soft blocks any LAN access as soon as the tunnel connection is established (by changing network\u0026rsquo;s routes on all local interfaces). There\u0026rsquo;s an option \u0026ldquo;enable LAN\u0026rdquo; in the Cisco VPN client, but it may be overriden by server\u0026rsquo;s rules.\nHere is how you can fully bypass thoses restrictions allowing you to access both VPN and local\u0026rsquo;s servers.\nFirst :follow instructions on \u0026ldquo;Lennart Schedin\u0026rdquo; \u0026rsquo;s blog : http://blog.lesc.se/2011/06/how-to-bypass-cisco-vpn-client-lan.html\nHe basically says:\nto install Shrew Soft VPN Client as Cisco VPN client replacement to add an exclude filter for your LAN\u0026rsquo;s ip range (right click on the connection / properties / Policy tab) Launch the connection Then : you can check using \u0026ldquo;route print\u0026rdquo; (on windows) : you\u0026rsquo;ll see that the VPN tunnel has not changed local routes. BUT, access to your LAN may still fail due to added routes\u0026hellip; At this time you are close to the final step : you just need to change routes. Below is an example BAT file to do so :\n@echo off set localroute=[SET YOUR DEFAULT LAN ROUTE HERE] set vpnroute=[SET YOUR DEFAULT VPN ROUTE HERE] REM flush all default routes route delete 0.0.0.0 mask 0.0.0.0 REM Add default route both for local network and VPN route add 0.0.0.0 mask 0.0.0.0 %localroute% route add 0.0.0.0 mask 0.0.0.0 %vpnroute% REM add specific routes if needed REM route add [IP RANGE TO ROUTE] mask [NETMASK OF THE IP RANGE] [IP OF THE ROUTE] ","permalink":"https://www.bluemind.org/network-fully-bypass-cisco-vpn-client-server-lan-restriction/","summary":"\u003cp\u003eYou may have encoutered, like me, some problem with LAN access while using the Cisco VPN Client.\u003c/p\u003e\n\u003cp\u003eThis soft blocks any LAN access as soon as the tunnel connection is established (by changing network\u0026rsquo;s routes on all local interfaces). There\u0026rsquo;s an option \u0026ldquo;enable LAN\u0026rdquo; in the Cisco VPN client, but it may be overriden by server\u0026rsquo;s rules.\u003c/p\u003e\n\u003cp\u003eHere is how you can fully bypass thoses restrictions allowing you to access both VPN and local\u0026rsquo;s servers.\u003c/p\u003e","title":"Network: How to fully bypass Cisco VPN Client and Server LAN restriction"},{"content":"I finaly managed to put the lastest touch on my customized turntable (based on a lenco L75S) : a retipped DL-103 Cartdridge with a shibata stilus and an aluminium body.\nThe sound is very clear and more detailled than with the original DL-103 ! If anyone is interested, I sent the cartdridge to the man behind the web site www.nadelspezialist.de and I bought the alu body on ebay : http://www.ebay.fr/itm/SOUND-IMPROVEMENTS-ALUMINIUM-BODY-DENON-103-103R-/320889445668?pt=Turntable_Parts_Accessories\u0026amp;hash=item4ab6805d24#ht_708wt_1185\n[nggallery id=3]\n","permalink":"https://www.bluemind.org/misc-final-touch-turntable/","summary":"\u003cp\u003eI finaly managed to put the lastest touch on my customized turntable (based on a lenco L75S) : a retipped DL-103 Cartdridge with a shibata stilus and an aluminium body.\u003c/p\u003e\n\u003cp\u003eThe sound is very clear and more detailled than with the original DL-103 !\nIf anyone is interested, I sent the cartdridge to the man behind the web site \u003ca href=\"https://www.nadelspezialist.de\"\u003ewww.nadelspezialist.de\u003c/a\u003e and I bought the alu body on ebay : \u003ca href=\"http://www.ebay.fr/itm/SOUND-IMPROVEMENTS-ALUMINIUM-BODY-DENON-103-103R-/320889445668?pt=Turntable_Parts_Accessories\u0026amp;hash=item4ab6805d24#ht_708wt_1185\"\u003ehttp://www.ebay.fr/itm/SOUND-IMPROVEMENTS-ALUMINIUM-BODY-DENON-103-103R-/320889445668?pt=Turntable_Parts_Accessories\u0026amp;hash=item4ab6805d24#ht_708wt_1185\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e[nggallery id=3]\u003c/p\u003e","title":"Music: final touch on my turntable"},{"content":"A few years ago when I used to play with C language on my Amiga, I started to create a Directory Opus Magellan like \u0026ldquo;lister\u0026rdquo; system.\nThe purpose was to keep this fantastic functionnality with an out-of-the-box amigaOS 3.9. I never finished this because when I upgraded to an AmigaPPC with MorphOS, a similar functionnality was already planned in this OS\u0026hellip;\nI recently found the source code so I put it here, for the fun ;). It was using Workbench 3.9 libraries as well as MUI 3.8 (If I remember well).\n","permalink":"https://www.bluemind.org/amiga-lister-dopus-magellan-lister-system-amigaos/","summary":"\u003cp\u003eA few years ago when I used to play with C language on my Amiga, I started to create a Directory Opus Magellan like \u0026ldquo;lister\u0026rdquo; system.\u003c/p\u003e\n\u003cp\u003eThe purpose was to keep this fantastic functionnality with an out-of-the-box amigaOS 3.9. I never finished this because when I upgraded to an AmigaPPC with MorphOS, a similar functionnality was already planned in this OS\u0026hellip;\u003c/p\u003e\n\u003cp\u003eI recently found the source code so I put it here, for the fun ;). It was using Workbench 3.9 libraries as well as MUI 3.8 (If I remember well).\u003c/p\u003e","title":"Projects: \"Lister\", a Dopus Magellan like lister system for amigaOS"},{"content":"[album id=1]\n","permalink":"https://www.bluemind.org/misc-galleries/","summary":"\u003cp\u003e[album id=1]\u003c/p\u003e","title":"Photo Galleries"},{"content":"Somes years ago, back in 2004, I restored an old Amiga 600 HD (this \u0026ldquo;HD\u0026rdquo; version pretty rare). I only have photos of it just before I reassembled it\u0026hellip; The Gallery is here\n","permalink":"https://www.bluemind.org/misc-a600hd-gallery-added/","summary":"\u003cp\u003eSomes years ago, back in 2004, I restored an old Amiga 600 HD (this \u0026ldquo;HD\u0026rdquo; version pretty rare). I only have photos of it just before I reassembled it\u0026hellip; The Gallery is \u003ca href=\"/misc-galleries/?album=1\u0026amp;gallery=1\" title=\"A600HD\"\u003ehere\u003c/a\u003e\u003c/p\u003e","title":"Amiga 600 HD Restoration Gallery"},{"content":"Using Zend_Db_Table to access database is a good way to retreive data. However, the pattern Zend_Db_Table implements (table data gateway) has, IMHO, a major drawback: it does not provide abstraction of table\u0026rsquo;s fields names.\nThis article describes a simple solution that I use\u0026hellip;\nIntrodution: the problem Providing tables\u0026rsquo; fields names abstraction is sometime also called \u0026ldquo;row mapping\u0026rdquo;. The purpose is to avoid using column\u0026rsquo;s name in the whole application to avoid dramatic impacts when the fields\u0026rsquo; names change.\nSome peopol does such mapping in the \u0026ldquo;service\u0026rdquo; layer. But this add some overhead to your application because the \u0026ldquo;service\u0026rdquo; layer have to do loop throught all record set to convert it to a \u0026ldquo;local\u0026rdquo; array with custom columns names.\nOn my side, I prefer to make a little mapping mecanism which address the columns names dependencies. Moreover, the method I use brings another bonus: access to data is done thought getter and setter and no more directly (or via __get() and __set()). This way, the code does not rely on columns names and it allows to add some logics to thoses methods if needed.\nA (simple) solution overriding method __call() of Zend_Db_Table_Row class Models_RowMapper extends Zend_Db_Table_Row { /** * Tries to map column name with setter and getter for * all methods that begins with \u0026#39;set\u0026#39; or \u0026#39;get\u0026#39;. * It uses getColumnName() method of the table class. * * If the call does not begin with \u0026#39;get\u0026#39; or \u0026#39;set\u0026#39;, we pass * the call to parent::__call() * @see Db/Table/Row/Zend_Db_Table_Row_Abstract#__call($method, $args) */ public function __call($methodName, $methodParams) { // first: separate get/set prefix and mapper name $prefix = substr($methodName, 0, 3); $name = $this-\u0026gt;getTable()-\u0026gt;getColumnName(substr($methodName, 3)); // handle get/set or call parent method if ($prefix == \u0026#39;get\u0026#39;) { return $this-\u0026gt;$name; } elseif ($prefix == \u0026#39;set\u0026#39;) { $this-\u0026gt;$name = $methodParams[0]; return $this; // provides fluid interface } else { return parent::__call($methodName, $methodParams); } } } in the following examples, we consider a \u0026ldquo;User\u0026rdquo; table with the followings columns: ID, NAME, FORENAME, EMAIL.\nclass Models_Base extends Zend_Db_Table_Abstract { /** * array to map database table columns to setter and getter methods * @var array */ protected $_columnsMapper = array(); /** * wrapper to parent::__construct that set a custom Row class * * @see Zend/Db/Table/Abstract#__construct * @param array $config */ public function __construct($config = array()) { parent::__construct($config); $this-\u0026gt;setRowClass(\u0026#39;Models_RowMapper\u0026#39;); } /** * get a column name by its mapper keyword (default: strtoupper($name)) * @param $name mapper keyword * @return string */ public function getColumnName($name) { if (array_key_exists($name, $this-\u0026gt;_columnsMapper)) { return $this-\u0026gt;_columnsMapper[$name]; } else { return strtoupper($name); } } } Going further Now imagine that the application is near finished and the DBA tell you that the columns of the \u0026ldquo;User\u0026rdquo; table has been renamed to : USER_NAME, USER_FORENAME and USER_EMAIL.\nUser the above solution, you just need to set the \u0026ldquo;_columnsMapper\u0026rdquo; attributs of \u0026ldquo;Models_User\u0026rdquo; to the following:\nprotected $_columnsMapper = array(\u0026#39;Name\u0026#39; =\u0026gt; \u0026#39;USER_NAME\u0026#39;, \u0026#39;ForeName =\u0026gt; \u0026#39;USER_FORENAME\u0026#39; \u0026#39;Email\u0026#39; =\u0026gt; \u0026#39;USER_EMAIL\u0026#39;); So now for example, \u0026ldquo;setName\u0026rdquo; will map the \u0026ldquo;USER_NAME\u0026rdquo; attribut.\nFinal word As usual, please don\u0026rsquo;t hesitate to correct me. Also, if something is not clear, I can try to correct it.\n","permalink":"https://www.bluemind.org/php-simple-model-mapper-zend_db_table/","summary":"\u003cp\u003eUsing Zend_Db_Table to access database is a good way to retreive data. However, the pattern Zend_Db_Table implements (table data gateway) has, IMHO, a major drawback: it does not provide abstraction of table\u0026rsquo;s fields names.\u003c/p\u003e\n\u003cp\u003eThis article describes a simple solution that I use\u0026hellip;\u003c/p\u003e\n\u003ch2 id=\"introdution-the-problem\"\u003eIntrodution: the problem\u003c/h2\u003e\n\u003cp\u003eProviding tables\u0026rsquo; fields names abstraction is sometime also called \u0026ldquo;row mapping\u0026rdquo;. The purpose is to avoid using column\u0026rsquo;s name in the whole application to avoid dramatic impacts when the fields\u0026rsquo; names change.\u003c/p\u003e","title":"PHP: a simple model mapper with Zend_Db_Table"},{"content":"If you use PHP and Zend Framwork, you may use Zend_Application, a marvelous componant that appeared in the 1.8 version of the framework.\nI just did some tests with it, and more precisely with ressources like translate and db. I had problem with the db ressource: no Zend_Adapter was initialised and thus, no adapter were set as the default one for Zend_DB_Table. It took me an hour of debugging to find the reason because I had just done a copy/paste from the reference guide and my application.ini was identical !\nWell, in fact, the problem was the copy / paste: a invisible but bad caracters (160) was inserted between \u0026ldquo;resources.db.adapter\u0026rdquo; and \u0026ldquo;=\u0026rdquo;. So the option \u0026ldquo;adapter\u0026rdquo; was in fact read as something like \u0026ldquo;adapter \u0026quot; (notice the space at the end) in PHP. And if you take a look at how ressource\u0026rsquo;s plugins work, you will see that the option \u0026ldquo;adapter\u0026rdquo; is concatenated with \u0026ldquo;set\u0026rdquo; to make a call to a method named \u0026ldquo;setadapter()\u0026rdquo;. Unfortunately, no error nor exception is thrown if the method does not exists. SO in my case the framework searched for \u0026ldquo;setadapter ()\u0026rdquo; which was not existant. So all dbAdapter intialisation was silently ignored\u0026hellip;\nI hope this could help someone somewhere ;)\n","permalink":"https://www.bluemind.org/php-zend_application-and-not-working-ressources/","summary":"\u003cp\u003eIf you use PHP and Zend Framwork, you may use Zend_Application, a marvelous componant that appeared in the 1.8 version of the framework.\u003c/p\u003e\n\u003cp\u003eI just did some tests with it, and more precisely with ressources like translate and db. I had problem with the db ressource: no Zend_Adapter was initialised and thus, no adapter were set as the default one for Zend_DB_Table. It took me an hour of debugging to find the reason because I had just done a copy/paste from the reference guide and my application.ini was identical !\u003c/p\u003e","title":"PHP: Zend_Application and not working ressources"},{"content":"This article presents a little overview of the MDA concept (Model Driven Achitecture). Moreover, thoses few words are the base reflection that led me to the creation of XMITransform\nLike all things found in this site, I wrote this small article with my very humble knownledge, so don\u0026rsquo;t hesitate to give me your feedback and / or advices.\nIntroduction : what is MDA ? MDA stands for \u0026ldquo;Model Driven Application\u0026rdquo;. To make it simple, theses terms mean creating software architecture from a model (ie: an UML class diagram) instead of doing it from the source code.\nOk, now you can tell me \u0026ldquo;I always do some work in UML before starting to code\u0026rdquo;. In fact, MDA approach goes a little futher by generating the source code of your architecture. To be more precise, the goal of MDA is to convert a generic and plateform independant model (eg: UML) into a plateform dependant architecture (eg: php or java implementation).\nFor example, you do all your application modelisation into UML diagrams (use cases, class diagrams, etc.). Then you choose what is the targeted plateform, and all the needed source code is generated.\nThe most used case is the UML class diagram to source code convertion (and vice-versa) . This is precisely the scope of this article (and the goal of XmiTransform)\nHowever, don\u0026rsquo;t be fooled: MDA will not write the logic for you. We are only talking about architecture, that is to say skeleton of the application. MDA will write a method skeleton (name, parameters, documentations, etc.) but not the code of the method.\n3 levels of MDA My very own vision of MDA is the following: we can consider that MDA complexity and power is divided in 3 levels.\nLevel 1: write This is the less complex level but also the less usefull. This does not means that this level is useless.\nSo this level is about transforming an UML model to sourcecode, plain, simple. I call it the write level because you can only write source code from the model. If you update the model you need to re-generate the source code and you loose, for example, any modification you made to the sources.\nDespite you can think that this level is very limited, it can helps you a lot: generating code can save you hours of code writing and syntax errors corrections. Moreover, if your UML diagram is well documented, your source code will be well prepared to be completed by another developper with minimal efforts.\nLevel 2 : read (reverse) The level two is a little more complexe than the first one. We can also call this level \u0026ldquo;the reverse level\u0026rdquo;.\nHere, we are talking about reverse engineering of your code to build the Model. Like any automatic reverse engineering job, it can be more or less done correctly, but it lets you modify your code and re-create the model without loosing your changes. Another very usefull functionnality is to generate an UML diagram of an application to better understand its conception.\nHowever, using this kind of functionnality will overwrite the model. And as I said earlier, automatic reverse engineering will never produce the same result as the original model that was used to create the source code.\nLevel 3 : Merge Level 3 is the more complex and also (IMHO) the more usefull MDA level. We can call this level \u0026ldquo;the merge level\u0026rdquo;.\nThe purpose is to be able to write and merge source code from the model as well as read source code to produce and merge changes to the model. To make it simple: any modification done on the model is merged with the existing source code, and any modification of the source code will produce changes in the existing model.\nThe real power of the MDA approach is here: modifying the model while your are coding is possible without loosing your code changes. In the other hand, coding some part of the architecture will be translated in the model. Thus, nothing is lost and constraints are minimals.\nExisting solutions There are differents available solutions: free, commercial, level 1, 2 or 3. As far as I know there are 2 kind of MDA softwares.\n1 - integrated solutions In general there are UML design softwares that allow you to transform your model into source code. The main limitations are:\nyou can only transform your model to languages supported by the software the generated source is abitrary formated (or at lesat, changing the format needs modifications of the software source code) As example we can name Bouml, Rose, Enterprise Architect, etc. Note that theses sofwares sometime offers some source code reverse engineering to build the model from sources.\n2 - Middleware solutions What I call middleware are solutions that only offer the functionnality of transforming model to source and / or source to model in an independant way.\nSuch solutions can be used thought their API by UML modeling software and programing IDE. Another way is to use standart file format (eg: xmi, which is UML explained in XML format).\nIn short: an middleware solution offer independant core functionnalities for transformation and allow you to write some plugins or drivers to modify the way things are generated. Thus you have more control at the price of a little more complicated way of doing.\nAs example we can name AndroMda or XmiTransform\nMy own middleware solution Based on the above statements, I first tryied to use AdroMDA for a powerfull PHP code generation. But making a generation module (this is called a cardrigde in androMDA language) is not that easy: it needs to read a big documentation, to follow a tutorial and write a dozen of java classes and velocity templates file.\nSo I decided to try to write my own solution. The final goal is to have a multi-language MDA middleware capable of merging (level 3).\nAs a first goal I tried to make code generation as easiest as possible keeping powerfull ablities. For now, XmiTransform is only capable of generating source code (level 2), but I think it is very simple to do so: you juste have to write one or more xsl file as well as a little xml file that organize your stylesheets.\nYou can check the project (XmiTransform) and even participate to it by giving me advices, remarks or help me in realization.\n","permalink":"https://www.bluemind.org/programming-mda-overview/","summary":"\u003cp\u003eThis article presents a little overview of the MDA concept (Model Driven Achitecture). Moreover, thoses few words are the base reflection that led me to the creation of \u003ca href=\"/projects-xmitransform-convert-xmi-source-code/\" title=\"XMITransform\"\u003eXMITransform\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eLike all things found in this site, I wrote this small article with my very humble knownledge, so don\u0026rsquo;t hesitate to give me your feedback and / or advices.\u003c/p\u003e\n\u003ch2 id=\"introduction--what-is-mda-\"\u003eIntroduction : what is MDA ?\u003c/h2\u003e\n\u003cp\u003eMDA stands for \u0026ldquo;Model Driven Application\u0026rdquo;. To make it simple, theses terms mean creating software architecture from a model (ie: an UML class diagram) instead of doing it from the source code.\u003c/p\u003e","title":"Programming: MDA Overview"},{"content":"The concept XMI Transform is a tool written in PHP (5) to convert XMI file to source code or source code to XMI file.The purpose is to provides a simple access to source code generation, which is, in general, the first thing we need in a MDA approach.\nTo do so, XMI Transform is based on XSL transformation and a \u0026ldquo;rule descriptor\u0026rdquo; in XML that simply helps to make a link between severals xsl stylesheets (one that enerate package name, one that build classes source code, etc.)\nHowever, transforming is a two step work and xsl stylesheet are the first step.The next step is to make use of what the stylesheer provides (eg, make directories or write files). This step is not as easy as transformation because it needs to be developed using PHP code. Such a thing is called an \u0026ldquo;engine\u0026rdquo;. For simple XMI to source transformations, the \u0026ldquo;Generic\u0026rdquo; engine will do the job fine. Nevertheless, if you need to handle merge over existing source code, or any other job others than making dir or writing file, you will need to write your own engine. But don\u0026rsquo;t be affraid, the main philosophy of XMI Transform is to keep things simple or users and developers. The code is fully object oriented and well documented.\nToday status XMI Transform is very young, but source code generation works fine. It is easily possible to make your own code generator.\nTransforming Source code to XMI is not working for now. But as it needs to parsesource code, specific read engines will be required for each language.\nChanges in latest release better php_zend model cleaned up the model code and splitted classContent.xsl in 3 files. It\u0026rsquo;s a little better now. added support for implicit dependency from extended class or implemented interface added support for BOUML \u0026rsquo;entity\u0026rsquo; stereotype to php_zend_model. They generate automatically zend_db_table_abstract derived classes with composition support (generate $_dependentTable and $_referenceMap, still need testing\u0026hellip;) added support for BOUML \u0026lsquo;control\u0026rsquo; stereotype to php_zend_model. They generate automatically zend_controller_action derived classes with naming convention respect (XxxController) added support for BOUML \u0026lsquo;action\u0026rsquo; method\u0026rsquo;s stereotype to php_zend_model (handle \u0026ldquo;Action\u0026rdquo; suffix for controllers\u0026rsquo; methods) added support for BOUML \u0026rsquo;exception\u0026rsquo; stereotype to php_zend_model. They generate automatically Exception derived classes added support for BOUML \u0026lsquo;form\u0026rsquo; and \u0026lsquo;dojoform\u0026rsquo; stereotype to php_zend_model. added support for multi-line comments and descriptions in classes and methods header (xmi must contains entity, not CR) corrected ZendLoader to Zend_Loader Xslt compiler errors are now trapped and converted to exception. So xtwr is now able to output a usable error message. various comments corrections some corrections in the ReadMe file. Future Next things to be done (non exhaustive):\nenhance php_zend model: add unit tests generation, support methods\u0026rsquo; params comments Make a simple java a C++ generator to give other examples write a first reader engine to be able to convert PHP source to XMI write a merge capable php writer engine add log support and little more verbose code add a check for needed php extensions The project is hosted at sourceforge http://sourceforge.net/projects/xmitransform/\n","permalink":"https://www.bluemind.org/projects-xmitransform-convert-xmi-source-code/","summary":"\u003ch2 id=\"the-concept\"\u003eThe concept\u003c/h2\u003e\n\u003cp\u003eXMI Transform is a tool written in PHP (5) to convert XMI file to source code or source code to XMI file.The purpose is to provides a simple access to source code generation, which is, in general, the first thing we need in a MDA approach.\u003c/p\u003e\n\u003cp\u003eTo do so, XMI Transform is based on XSL transformation and a \u0026ldquo;rule descriptor\u0026rdquo; in XML that simply helps to make a link between severals xsl stylesheets (one that enerate package name, one that build classes source code, etc.)\u003c/p\u003e","title":"Projects: XmiTransform, convert XMI to source code"},{"content":"I just found 2 little pieces of codes I made some time ago to check how easy it was to read and write excel sheet in Visual Basic.\nReading Dim Cn Dim File Dim rst \u0026#39; init ADODB object for reading excel file... Set Cn = CreateObject(\u0026#34;ADODB.Connection\u0026#34;) Set rst = CreateObject(\u0026#34;ADODB.Recordset\u0026#34;) \u0026#39; ...using jet driver Cn.open \u0026#34;Provider=Microsoft.Jet.OLEDB.4.0; Data Source=C:\\SheetFile.xls;Extended Properties=\u0026#34;\u0026#34;Excel 8.0;HDR=NO\u0026#34;\u0026#34;\u0026#34; \u0026#39;strConnect \u0026#39; we just get current time for benchmark purposes startTime = Time() \u0026#39; reading some fields is just a matter of a simple SQL select Set rst = Cn.Execute(\u0026#34;Select * from [Sheet1$]\u0026#34;) \u0026#39; here we loop over all non-empty lines and print column A, B, C and D Do while(not rst.EOF) WScript.echo(rst.Fields(0) \u0026amp; \u0026#34; - \u0026#34; \u0026amp; rst.Fields(1) \u0026amp; \u0026#34; - \u0026#34; \u0026amp; rst.Fields(2) \u0026amp; \u0026#34; - \u0026#34; \u0026amp; rst.Fields(3)) rst.moveNext Loop \u0026#39; some date comparison for benchmark purposes endTime = DateDiff(\u0026#34;s\u0026#34;,startTime,Time()) WScript.echo(\u0026#34;Read file in \u0026#34; \u0026amp; endTime \u0026amp; \u0026#34; sec\u0026#34;) \u0026#39; Close the connection Cn.Close Set Cn = Nothing Writing Dim Cn Dim rst \u0026#39; init ADODB object for writing to excel file... Set Cn = CreateObject(\u0026#34;ADODB.Connection\u0026#34;) Set rst = CreateObject(\u0026#34;ADODB.Recordset\u0026#34;) \u0026#39;...using adodb driver With Cn .Provider = \u0026#34;MSDASQL\u0026#34; .ConnectionString = \u0026#34;Driver={Microsoft Excel Driver (*.xls)};\u0026#34; \u0026amp; _\u0026#34;DBQ=C:\\SheetFile.xls\u0026#34; \u0026amp; \u0026#34;; ReadOnly=False;\u0026#34; .Open End With \u0026#39; we just get current time for benchmark purposes startTime = Time() \u0026#39; gets the current time \u0026#39; fill 100 lines in the column A of sheet 1 For i = 1 to 100 set rst = Cn.Execute(\u0026#34;Insert into [Sheet1$A\u0026#34;\u0026amp; i \u0026amp;\u0026#34;:B\u0026#34;\u0026amp; i \u0026amp;\u0026#34;] values(\u0026#34;\u0026amp; i \u0026amp;\u0026#34;,\u0026#34;\u0026amp; i \u0026amp;\u0026#34;)\u0026#34;) Next \u0026#39; some date comparison for benchmark purposes endTime = DateDiff(\u0026#34;s\u0026#34;,startTime,Time()) WScript.echo(\u0026#34;inserted \u0026#34; \u0026amp; i \u0026amp; \u0026#34; rows in \u0026#34; \u0026amp; endTime \u0026amp; \u0026#34; sec\u0026#34;) \u0026#39; Close the connection Cn.Close Set Cn = Nothing ","permalink":"https://www.bluemind.org/vbs-reading-and-writing-excel-sheet/","summary":"\u003cp\u003eI just found 2 little pieces of codes I made some time ago to check how easy it was to read and write excel sheet in Visual Basic.\u003c/p\u003e\n\u003ch2 id=\"reading\"\u003eReading\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eDim Cn\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eDim File\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eDim rst\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39; init ADODB object for reading excel file...\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSet Cn = CreateObject(\u0026#34;ADODB.Connection\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSet rst = CreateObject(\u0026#34;ADODB.Recordset\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39; ...using  jet driver\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCn.open \u0026#34;Provider=Microsoft.Jet.OLEDB.4.0; Data Source=C:\\SheetFile.xls;Extended Properties=\u0026#34;\u0026#34;Excel 8.0;HDR=NO\u0026#34;\u0026#34;\u0026#34; \u0026#39;strConnect\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39; we just get current time for benchmark purposes\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003estartTime = Time()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39; reading some fields is just a matter of a simple SQL select\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSet rst = Cn.Execute(\u0026#34;Select * from [Sheet1$]\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39; here we loop over all non-empty lines and print column A, B, C and D\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eDo while(not rst.EOF)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    WScript.echo(rst.Fields(0) \u0026amp; \u0026#34; - \u0026#34; \u0026amp; rst.Fields(1) \u0026amp; \u0026#34; - \u0026#34; \u0026amp; rst.Fields(2) \u0026amp; \u0026#34; - \u0026#34; \u0026amp; rst.Fields(3))\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    rst.moveNext\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eLoop\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39; some date comparison for benchmark purposes\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eendTime = DateDiff(\u0026#34;s\u0026#34;,startTime,Time())\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eWScript.echo(\u0026#34;Read file in \u0026#34; \u0026amp; endTime \u0026amp; \u0026#34; sec\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39; Close the connection\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCn.Close\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSet Cn = Nothing\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"writing\"\u003eWriting\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eDim Cn\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eDim rst\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39; init ADODB object for writing to excel file...\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSet Cn = CreateObject(\u0026#34;ADODB.Connection\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSet rst = CreateObject(\u0026#34;ADODB.Recordset\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39;...using adodb driver\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eWith Cn\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    .Provider = \u0026#34;MSDASQL\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    .ConnectionString = \u0026#34;Driver={Microsoft Excel Driver (*.xls)};\u0026#34; \u0026amp; _\u0026#34;DBQ=C:\\SheetFile.xls\u0026#34;  \u0026amp; \u0026#34;; ReadOnly=False;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    .Open\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eEnd With\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39; we just get current time for benchmark purposes\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003estartTime = Time() \u0026#39; gets the current time\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39;  fill 100 lines in the column A of sheet 1\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eFor i = 1 to 100\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tset rst = Cn.Execute(\u0026#34;Insert into [Sheet1$A\u0026#34;\u0026amp; i \u0026amp;\u0026#34;:B\u0026#34;\u0026amp; i \u0026amp;\u0026#34;] values(\u0026#34;\u0026amp; i \u0026amp;\u0026#34;,\u0026#34;\u0026amp; i \u0026amp;\u0026#34;)\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eNext\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39; some date comparison for benchmark purposes\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eendTime = DateDiff(\u0026#34;s\u0026#34;,startTime,Time())\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eWScript.echo(\u0026#34;inserted \u0026#34; \u0026amp; i \u0026amp; \u0026#34; rows in \u0026#34; \u0026amp; endTime \u0026amp; \u0026#34; sec\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026#39; Close the connection\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCn.Close\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSet Cn = Nothing\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"VBS: reading and writing excel sheet"},{"content":"I recently reformated my laptop hardrive to use ArchLinux instead of Gentoo. So I decided to make a special partition to run WinXP natively as well as under virtualbox. I just explain a little the steps I followed to make it working\u0026hellip;\nPartitionning First, you need to partition your harddrive correctly. That is to say that any bootable partition must be primary.\nAs a simple example here is my partition scheme for a 160Gb drive:\n/dev/sda1 : Primary, /boot (ext2fs, 180 Mb) /dev/sda2 : Extended Master /dev/sda3 : Primary, MacOSX (HFS+, 20Gb) /dev/sda4 : Primary, WinXP (FAT32, 9Gb) /dev/sda5 : Extended, / (XFS, 20Gb) /dev/sda6 : Extended, /var (ReiserFS, 9.5Gb) /dev/sda7 : Extended, /home (XFS, 95Gb) /dev/sda8 : Extended, Linux swap\nAs you can see, I use /dev/sda4 as my windows XP partition.\nA Virtual disk as partition wrapper Virtualbox is not able to use physical partition easily out of the box. In fact you have to create a fake disk image that define how to access the physical partition.\nMaking a pseudo MBR If you want to make the partition bootable you will need to make a pseudo MBR (Master boot record). The package \u0026ldquo;ms-sys\u0026rdquo; contains the tool to build one:\npacman -S ms-sys # install ms-sys package touch ~/.VirtualBox/WindowsXP.mbr # the file that will receive the MBR must exists... ms-sys --mbr -f ~/.VirtualBox/WindowsXP.mbr # create a MBR and store it in a file Making the fake disk image To do so, open terminal and be sure that your user is part of the \u0026ldquo;vboxusers\u0026rdquo; and \u0026ldquo;disk\u0026rdquo; groups. Then use VBoxManage (for /dev/sda4):\nVBoxManage internalcommands createrawvmdk -filename ~/.VirtualBox/VDI/winxp_sda4.vmdk \\ -rawdisk /dev/sda -partitions 4 -relative -register -mbr ~/.VirtualBox/WindowsXP.mbr just after \u0026ldquo;-filename\u0026rdquo;, you can see the virtual disk file that will represent the VirtualBox disk. \u0026ldquo;-rawdisk /dev/sda -partitions 4\u0026rdquo; is for using /dev/sda4\nUsing the new partition Create a new virtual machine and choose the existing harddisk we just created. When the VM wizard has finished, select the VM and edit its setting to turn on \u0026ldquo;IO APIC\u0026rdquo;.\nYou can install windows XP either using the VM or booting natively.\nAs soon as Windows is installed, go to settings / system and open the hardware list windows. find the IDE controller and change the driver to be the \u0026ldquo;generic IDE\u0026rdquo;.\nThen, create a new hardware profile. You can reboot the computer or th VM. You will now have to choose the profile to use before booting.\nNow you can install all needed drivers for each profile, but do not try to boot VirtualBox with the profile used for the hardware boot, else it wont work (and vice-versa).\n","permalink":"https://www.bluemind.org/linux-virtualbox-using-physical-bootable-partition/","summary":"\u003cp\u003eI recently reformated my laptop hardrive to use ArchLinux instead of Gentoo. So I decided to make a special partition to run WinXP natively as well as under virtualbox. I just explain a little the steps I followed to make it working\u0026hellip;\u003c/p\u003e\n\u003ch2 id=\"partitionning\"\u003ePartitionning\u003c/h2\u003e\n\u003cp\u003eFirst, you need to partition your harddrive correctly. That is to say that any bootable partition must be primary.\u003c/p\u003e\n\u003cp\u003eAs a simple example here is my partition scheme for a 160Gb drive:\u003c/p\u003e","title":"Linux: virtualbox using physical bootable partition"},{"content":"\nThere\u0026rsquo;s sometime ago I wanted to see how java 2D was working.\nI came from Amiga computers, where doing double buffering was part of your job in C. But using accellerated 2D java engine seems a lot simpler as it do the job for you ;)\nBelow is a little java class I coded to experiment java 2D with double buffering.\nIt may be wrong now since this code is quite old and its also a lot slower than the same code on an Amiga 4000 using C language.\nimport javax.swing.*; import java.awt.*; import java.awt.image.*; import java.awt.event.*; // main window public class MainWindow extends Canvas { /* The stragey that allows us to use accelerate page flipping */ private BufferStrategy strategy; // constructor public MainWindow() { // create window JFrame container = new JFrame(\u0026#34;Java 2d test\u0026#34;); // get hold the content of the frame and set up the resolution of the game JPanel panel = (JPanel) container.getContentPane(); panel.setPreferredSize(new Dimension(800,600)); panel.setLayout(null); // setup our canvas size and put it into the content of the frame setBounds(0,0,800,600); panel.add(this); // Tell AWT not to bother repainting our canvas since we\u0026#39;re // going to do that our self in accelerated mode setIgnoreRepaint(true); // finally make the window visible container.pack(); container.setResizable(false); container.setVisible(true); // add a listener to respond to the user closing the window. If they // do we\u0026#39;d like to exit the game container.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); addKeyListener(new keyhandler()); // get focus to receive key events requestFocus(); // create the buffering strategy which will allow AWT // to manage our accelerated graphics createBufferStrategy(2); strategy = getBufferStrategy(); // start Draw(); } public void Draw() { int j = 0 , i = 0, r = 255,g = 255, b=255, mr = -1,mg = -1,mb = -1; int endw = 800, endh = 600, mw=1, mh = -1; // Get hold of a graphics context for the accelerated // surface and blank it out Graphics2D gfx = (Graphics2D) strategy.getDrawGraphics(); gfx.setColor(Color.black); gfx.fillRect(0,0,endw,endh); gfx.setColor(Color.white); while(true) { gfx.setColor(new Color(r,g,b)); gfx.drawLine(i,j,endw-i,endh-j); //gfx.draw3DRect(i,endh,i,endw,false); gfx.drawArc(i, j, endw, endh, 45-i, 90+j); // finally, we\u0026#39;ve completed drawing so clear up the graphics // and flip the buffer over //g.dispose(); strategy.show(); if(i \u0026lt; 800) i++; else { j++; if(j \u0026gt; 600) { j = 0; i = 0; } } r = r + (2*mr); g = g + (5*mg); b = b + (10*mb); if(r \u0026lt;= 5) mr = 1; else if(r \u0026gt;= 250) mr = -1; if(g \u0026lt;= 5) mg = 1; else if(g \u0026gt;= 250) mg = -1; if(b \u0026lt;= 5) mb = 1; else if(b \u0026gt;= 250) mb = -1; endw+=mw; endh+=mh; if(endw \u0026gt;= 1200) mw = -1; else if(endw \u0026lt;= 300) mw = 1; if(endh \u0026gt;= 900) mh = -1; else if(endh \u0026lt;= 50) mh = 1; try { Thread.sleep(1); } catch (Exception e) {} } } private class keyhandler extends KeyAdapter { public void keyTyped(KeyEvent e) { System.exit(0); } } // main function public static void main(String[] args) { new MainWindow(); } } ","permalink":"https://www.bluemind.org/java-playing-java-2d/","summary":"\u003cp\u003e\u003cimg\n  src=\"images/java2d.png\"\n  alt=\"Java 2D double-buffering render\"\n  class=\"article-image\"\n  loading=\"lazy\"\n\u003e\u003c/p\u003e\n\u003cp\u003eThere\u0026rsquo;s sometime ago I wanted to see how java 2D was working.\u003c/p\u003e\n\u003cp\u003eI came from Amiga computers, where doing double buffering was part of your job in C.\nBut using accellerated 2D java engine seems a lot simpler as it do the job for you ;)\u003c/p\u003e\n\u003cp\u003eBelow is a little java class I coded to experiment java 2D with double buffering.\u003c/p\u003e\n\u003cp\u003eIt may be wrong now since this code is quite old and its also a lot slower than the same\ncode on an Amiga 4000 using C language.\u003c/p\u003e","title":"Java: playing with Java 2D"},{"content":"Description YAG Genesis was made some years ago on my Amiga 4000 PPC. I used StormC 3 to compile it (the stormC project file is included in the source archive). Below is the description from the Aminet readme file.\nThis is another GUI dedicated to Amigenerator, or I could say Yet another ! :) It was written for those who do not like \u0026ldquo;All-in-one\u0026rdquo; GUI (such as the great Nostalgia) or simply never found a GUI with theses main features:\nPreview of the selected game (screenshot or anything else) Rom informations (Rom name, copyright and version) you can select your Amigenrator executable All amigenerator parameters are configurables and bubble help described Fully localised Activate or De-activate GUI elements (preview, rom info, status) To works, the following is needed : AmigaOS 3.x MUI 3.8 (not tested on earlier version) Guigfx.library Guigfx for mui [caption id=\u0026ldquo;attachment_100\u0026rdquo; align=\u0026ldquo;aligncenter\u0026rdquo; width=\u0026ldquo;450\u0026rdquo; caption=\u0026ldquo;Preferences windows\u0026rdquo;] [/caption]\n","permalink":"https://www.bluemind.org/projects-yag-genesis-amiga/","summary":"\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"images/Yag_Genesis-preview.jpg\"\u003e\u003cimg\n  src=\"images/Yag_Genesis-preview.jpg\"\n  alt=\"\"\n  class=\"article-image\"\n  loading=\"lazy\"\n\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eYAG Genesis was made some years ago on my Amiga 4000 PPC. I used StormC 3 to compile it (the stormC project file is included in the source archive). Below is the description from the Aminet readme file.\u003c/p\u003e\n\u003cp\u003eThis is another GUI dedicated to Amigenerator, or I could say Yet another ! :) It was written for those who do not like \u0026ldquo;All-in-one\u0026rdquo; GUI (such as the great Nostalgia) or simply never found a GUI with theses main features:\u003c/p\u003e","title":"Projects: YAG Genesis for Amiga"},{"content":"Description BootManager was written a few years ago, when I was student. It has been made on an Amiga 4000 PPC using stormC 3. Bellow is the description from the Aminet readme file.\nThis is a simple program wich allows you to boot from another startup-sequence by pressing one of the 10 fkeys or one of the three mouse buttons while booting. You can execute 13 differents startup-sequence files, depending on wich button you press. If you press the HELP key, your startu\np-sequence will be executed step by step, very usefull to find a problem :)\nIf no button are pressed, your startup-sequence will be executed normaly so your Amiga will boot as usual. But if you press a button to which you\u0026rsquo;ve assigned another startup file, this last will be executed\nwhile the real startup-sequence won\u0026rsquo;t.\nSo this is the fastest way to have multi-boot without running a menu or something else at the boot.\nI use it to boot MacOS via Shapeshifter in \u0026ldquo;quick mode\u0026rdquo; without loading the WB or booting with \u0026ldquo;no startup sequence\u0026rdquo;. By this way, the MacOS boot like if I had a real Mac :). I use it also to boot linux PPC directly without starting the workbench, thus it look like a real OS that boot without the help of another OS.\nRequirements : An Amiga with an hard drive (this program is not very usefull without an hardrive !) MUI 3.8 is needed for the prefs program (not tested on earlier versions) The main program should work on any version of the Workbench (tested only on 3.x) ","permalink":"https://www.bluemind.org/project-bootmanager-for-amiga/","summary":"\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"images/BootManager-Preview.jpg\"\u003e\u003cimg\n  src=\"images/BootManager-Preview.jpg\"\n  alt=\"\"\n  class=\"article-image\"\n  loading=\"lazy\"\n\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eBootManager was written a few years ago, when I was student. It has been made on an Amiga 4000 PPC using stormC 3. Bellow is the description from the \u003cem\u003eAminet readme\u003c/em\u003e file.\u003c/p\u003e\n\u003cp\u003eThis is a simple program wich allows you to boot from another startup-sequence by pressing one of the 10 fkeys or one of the three mouse buttons while booting. You can execute 13 differents startup-sequence files, depending on wich button you press. If you press the HELP key, your startu\u003c/p\u003e","title":"Projects : BootManager for Amiga"},{"content":" There\u0026rsquo;s some years ago, during the war between IE4 and Netscape 4.5 (summer 2001 if I remember well), a friend and I had to learn Javascript for a job. We known nothing about this language nor its possibilities. So we decided to make an Arkanoid like game (a ball that break bricks).\nAfter some days to discover the game logics and the javascript languages, we ended in a little game, ugly and badly programmed, but it worked ;)\nSome years later, I decided to make a little better version with some new (and still ugly) graphics and little better programming. This is the version you can play here. It uses an old Javascript framework I made\u0026hellip;\nThis Game is not really finished, and It is far from having the same functionalities as Arkanoid. It\u0026rsquo; not really special for today\u0026rsquo;s browser and JS engine, but it was a little more fun in 2004 when a PIII 500Mhz needed to skip frames in order to keep the game playable\u0026hellip; ;)\nLaunch the game now !\n","permalink":"https://www.bluemind.org/projects-jsbricks-an-arkanoid-clone-in-javascript/","summary":"\u003cp\u003e\u003ca href=\"images/jsbricks-preview.png\"\u003e\u003cimg\n  src=\"images/jsbricks-preview.png\"\n  alt=\"\"\n  class=\"article-image\"\n  loading=\"lazy\"\n\u003e\u003c/a\u003e There\u0026rsquo;s some years ago, during the war between IE4 and Netscape 4.5 (summer 2001 if I remember well), a friend and I had to learn Javascript for a job. We known nothing about this language nor its possibilities. So we decided to make an Arkanoid like game (a ball that break bricks).\u003c/p\u003e\n\u003cp\u003eAfter some days to discover the game logics and the javascript languages, we ended in a little game, ugly and badly programmed, but it worked ;)\u003c/p\u003e","title":"Projects: JSBricks, an arkanoid clone in javascript"},{"content":"The following shell script can be used as a cron job. It will backup all databases with a unique name based on date and time. Backup files are rotated automatically (delete old backup). This script is based on another one written by Etienne Pouliot: http://www.defitek.com/blog/2010/01/06/a-simple-yet-effective-postgresql-backup-script/\n#!/bin/sh # Posrgres executables CMD_PSQL=/usr/local/pgsql/bin/psql CMD_DUMP=/usr/local/pgsql/bin/pg_dump # prefix for backup filenames NAME_PREFIX=`date +%j` # directory where to save the database backup NAME_BACKUP_DIR=/tmp # age of the older file to keep (in days) NB_DAYS_TO_KEEP=7 # Start backuping Databases=`$CMD_PSQL -tq -d template1 -c \u0026#34;select datname from pg_database\u0026#34;` echo \u0026#34;Starting backup of all databases...\u0026#34; for current_db in `echo $Databases` do $CMD_DUMP -D $current_db \u0026gt; /$NAME_BACKUP_DIR/$NAME_PREFIX.$current_db.backup done echo \u0026#34;Backup finnished !\u0026#34; echo \u0026#34; \u0026#34; # start deleting old file echo \u0026#34;Deleteting old backup files...\u0026#34; oldbackup=`find $NAME_BACKUP_DIR -type f -mtime +$NB_DAYS_TO_KEEP -name \u0026#34;*.backup\u0026#34;` for current_file in `echo $oldbackup` do rm -f $current_file echo \u0026#34;$current_file deleted\u0026#34; done echo \u0026#34;Old file deletion finished !\u0026#34; echo \u0026#34; \u0026#34; ","permalink":"https://www.bluemind.org/postgresql-backup-script-for-all-databases/","summary":"\u003cp\u003eThe following shell script can be used as a cron job. It will backup all databases with a unique name based on date and time. Backup files are rotated automatically (delete old backup). This script is based on another one written by Etienne Pouliot: \u003ca href=\"http://www.defitek.com/blog/2010/01/06/a-simple-yet-effective-postgresql-backup-script/\" title=\"a-simple-yet-effective-postgresql-backup-script\"\u003ehttp://www.defitek.com/blog/2010/01/06/a-simple-yet-effective-postgresql-backup-script/\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#!/bin/sh\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Posrgres executables\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCMD_PSQL\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e/usr/local/pgsql/bin/psql\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCMD_DUMP\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e/usr/local/pgsql/bin/pg_dump\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# prefix for backup filenames\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eNAME_PREFIX\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003edate +%j\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# directory where to save the database backup\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eNAME_BACKUP_DIR\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e/tmp\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# age of the older file to keep (in days)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eNB_DAYS_TO_KEEP\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e7\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Start backuping\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eDatabases\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e$CMD_PSQL -tq -d template1 -c \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;select datname from pg_database\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Starting backup of all databases...\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e current_db in \u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003eecho $Databases\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003edo\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  $CMD_DUMP -D $current_db \u0026gt; /$NAME_BACKUP_DIR/$NAME_PREFIX.$current_db.backup\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003edone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Backup finnished !\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#e6db74\"\u003e\u0026#34; \u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# start deleting old file\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Deleteting old backup files...\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eoldbackup\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003efind $NAME_BACKUP_DIR -type f -mtime +$NB_DAYS_TO_KEEP -name \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;*.backup\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e current_file in \u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003eecho $oldbackup\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003edo\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  rm -f $current_file\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  echo \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e$current_file\u003cspan style=\"color:#e6db74\"\u003e deleted\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003edone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Old file deletion finished !\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#e6db74\"\u003e\u0026#34; \u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"PostgreSQL: backup script for all databases"},{"content":"Sometime a process can take 100% of all available cpu power because it crashed. Here is a little script I used on a server for pdftk because this software was crashing from time to time.\nJust launch the script from a cron job and replace \u0026ldquo;PROCESS_NAME\u0026rdquo; by the name of the process to watch.\nargument 1 : number of minutes after which the process will be killed argument 2 : number of seconds #!/bin/bash # get process id and execution time process=`top -b -n 1 | grep PROCESS_NAME | awk \u0026#39;{print $1\u0026#34; \u0026#34;$11}\u0026#39;` # extract pid pid=`echo $process | awk \u0026#39;{print $1}\u0026#39;` # extract execution time time=`echo $process | awk \u0026#39;{print $2}\u0026#39; | awk -F \u0026#34;:\u0026#34; \u0026#39;{ min=$1; sec=$2; print 60*min+sec}\u0026#39;` if [ $(echo \u0026#34;$time \u0026gt; 1\u0026#34;|bc) -eq 1 ] ; then kill -9 $pid echo \u0026#34;killed process $pid that used $time of processor power\u0026#34; fi ","permalink":"https://www.bluemind.org/linux-autokill-a-crashed-infinite-loop-process/","summary":"\u003cp\u003eSometime a process can take 100% of all available cpu power because it crashed. Here is a little script I used on a server for pdftk because this software was crashing from time to time.\u003c/p\u003e\n\u003cp\u003eJust launch the script from a cron job and replace \u0026ldquo;PROCESS_NAME\u0026rdquo; by the name of the process to watch.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eargument 1 : number of minutes after which the process will be killed\u003c/li\u003e\n\u003cli\u003eargument 2 : number of seconds\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#!/bin/bash\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# get process id and execution time\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eprocess\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003etop -b -n \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e | grep PROCESS_NAME | awk \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;{print $1\u0026#34; \u0026#34;$11}\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# extract pid\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epid\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003eecho $process | awk \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;{print $1}\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# extract execution time\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etime\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003eecho $process | awk \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;{print $2}\u0026#39;\u003c/span\u003e | awk -F \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;:\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;{ min=$1; sec=$2; print 60*min+sec}\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e[\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003e$(\u003c/span\u003eecho \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e$time\u003cspan style=\"color:#e6db74\"\u003e \u0026gt; 1\u0026#34;\u003c/span\u003e|bc\u003cspan style=\"color:#66d9ef\"\u003e)\u003c/span\u003e -eq \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e]\u003c/span\u003e ;  \u003cspan style=\"color:#66d9ef\"\u003ethen\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  kill -9 $pid\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  echo \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;killed process \u003c/span\u003e$pid\u003cspan style=\"color:#e6db74\"\u003e that used \u003c/span\u003e$time\u003cspan style=\"color:#e6db74\"\u003e of processor power\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efi\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Linux: autokill a crashed / infinite loop process"},{"content":"Beware that the following contains a severe security issue if not well used.\nA tiny command in C source code int main() { execlp(\u0026#34;login\u0026#34;,\u0026#34;login\u0026#34;,\u0026#34;-f\u0026#34;,\u0026#34;YOUR LOGIN HERE\u0026#34;,0); } compilation Just use gcc :\ngcc -o autologin autologin.c Usage First, Copy the compiled command to /usr/sbin. Then, edit you inittab file (eg. /etc/inittab) and modify to have line like the following :\n1:2345:respawn:/sbin/getty -n -l /usr/sbin/autologin 38400 tty1 You can always log-in as a different user by using another virtual term or any ssh connection.\nExtras Now that your user is automatically logged in, you can execute any command after you have been logged using your bash profile. For exemple adding the following in you ~/bash_profile will automatically log you in if current tty is tty1:\nif [ -z \u0026#34;$DISPLAY\u0026#34; ] \u0026amp;\u0026amp; [ $(tty) == /dev/tty1 ]; then startx fi This will automaticaly launch X when the current used tty is /dev/tty1\n","permalink":"https://www.bluemind.org/linux-auto-login-without-xdm/","summary":"\u003cp\u003eBeware that the following contains a severe security issue if not well used.\u003c/p\u003e\n\u003ch2 id=\"a-tiny-command-in-c\"\u003eA tiny command in C\u003c/h2\u003e\n\u003ch3 id=\"source-code\"\u003esource code\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eexeclp\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;login\u0026#34;\u003c/span\u003e,\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;login\u0026#34;\u003c/span\u003e,\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;-f\u0026#34;\u003c/span\u003e,\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;YOUR LOGIN HERE\u0026#34;\u003c/span\u003e,\u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"compilation\"\u003ecompilation\u003c/h3\u003e\n\u003cp\u003eJust use gcc :\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    gcc -o autologin autologin.c\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"usage\"\u003eUsage\u003c/h2\u003e\n\u003cp\u003eFirst, Copy the compiled command to /usr/sbin. Then, edit you inittab file (eg. /etc/inittab) and modify to have line like the following :\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    1:2345:respawn:/sbin/getty -n -l /usr/sbin/autologin 38400 tty1\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eYou can always log-in as a different user by using another virtual term or any ssh connection.\u003c/p\u003e","title":"Linux: Auto Login without XDM"},{"content":"The Comtend CT-633 is a small DSL router that was used by some companies (at least in France). Undortunatly, some usefull features were hidden.\nHidden pages bridges wlan / lan: http://192.168.1.1/bridgelist.html tftp : http://192.168.1.1/tftp.html acl list : http://192.168.1.1/acllist.html proxies : http://192.168.1.1/proxyconfig.html user list : http://192.168.1.1/userlist.html List of bridge entries BRIDGE http://192.168.1.1/bridgelist.html List of spaning tree entries SPANING TREE http://192.168.1.1/spaninglist.html List of filter entries FILTERS http://192.168.1.1/filterlist.html Bridge L2 filter L2 FILTER http://192.168.1.1/bridgel2filter.html Routing setup + List of static routes ROUTING SETUP http://192.168.1.1/routinglist.html DHCP relay configuration DHCP RELAY http://192.168.1.1/dhcprelay.html List of DHCP entries DHCP SERVER http://192.168.1.1/dhcplist.html DHCP client configuration entries DHCP CLIENT http://192.168.1.1/dhcpclient.html List of WFQ traffic parameters WFQ http://192.168.1.1/wfqlist.html Traffic conditioning list TRAFFIC CONDITIONING http://192.168.1.1/tclist.html List of SNMP parameters SYSTEM http://192.168.1.1/snmplist.html List of TRAP server entries TRAPS http://192.168.1.1/snmptraplist.html List of communities entries COMMUNITIES http://192.168.1.1/snmpcommunitylist.html List of IGMP proxy entries IGMP PROXY http://192.168.1.1/igmplist.html IGMP Proxy configuration IGMP PROXY http://192.168.1.1/igmpconfig.html SNTP Configuration SNTP http://192.168.1.1/sntp.html Hidden features Mac address filter connect to the router in telnet :\nwlancfg set AccessControlList=11:11:11:11:11:11:11:11;22:22:22:22:22:22 wlancfg get AccessPolicy=1 home save exit where 11:11:11:11:11:11:11:11 and 22:22:22:22:22:22 are mac addresses. Up to 32 addresses can be specified. stop ringing connect to the router in telnet :\nmgcp PSTNcall NO home save exit ","permalink":"https://www.bluemind.org/hardware-comtrend-ct-633-hidden-things/","summary":"\u003cp\u003eThe Comtend CT-633 is a small DSL router that was used by some companies (at least in France). Undortunatly, some usefull features were hidden.\u003c/p\u003e\n\u003ch2 id=\"hidden-pages\"\u003eHidden pages\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003ebridges wlan / lan: \u003ca href=\"http://192.168.1.1/bridgelist.html\" title=\"http://192.168.1.1/bridgelist.html\"\u003ehttp://192.168.1.1/bridgelist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003etftp : \u003ca href=\"http://192.168.1.1/tftp.html\" title=\"http://192.168.1.1/tftp.html\"\u003ehttp://192.168.1.1/tftp.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eacl list : \u003ca href=\"http://192.168.1.1/acllist.html\" title=\"http://192.168.1.1/acllist.html\"\u003ehttp://192.168.1.1/acllist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eproxies : \u003ca href=\"http://192.168.1.1/proxyconfig.html\" title=\"http://192.168.1.1/proxyconfig.html\"\u003ehttp://192.168.1.1/proxyconfig.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003euser list : \u003ca href=\"http://192.168.1.1/userlist.html\" title=\"http://192.168.1.1/userlist.html\"\u003ehttp://192.168.1.1/userlist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eList of bridge entries BRIDGE \u003ca href=\"http://192.168.1.1/bridgelist.html\" title=\"http://192.168.1.1/bridgelist.html\"\u003ehttp://192.168.1.1/bridgelist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eList of spaning tree entries SPANING TREE \u003ca href=\"http://192.168.1.1/spaninglist.html\" title=\"http://192.168.1.1/spaninglist.html\"\u003ehttp://192.168.1.1/spaninglist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eList of filter entries FILTERS \u003ca href=\"http://192.168.1.1/filterlist.html\" title=\"http://192.168.1.1/filterlist.html\"\u003ehttp://192.168.1.1/filterlist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eBridge L2 filter L2 FILTER \u003ca href=\"http://192.168.1.1/bridgel2filter.html\" title=\"http://192.168.1.1/bridgel2filter.html\"\u003ehttp://192.168.1.1/bridgel2filter.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eRouting setup + List of static routes ROUTING SETUP \u003ca href=\"http://192.168.1.1/routinglist.html\" title=\"http://192.168.1.1/routinglist.html\"\u003ehttp://192.168.1.1/routinglist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eDHCP relay configuration DHCP RELAY \u003ca href=\"http://192.168.1.1/dhcprelay.html\" title=\"http://192.168.1.1/dhcprelay.html\"\u003ehttp://192.168.1.1/dhcprelay.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eList of DHCP entries DHCP SERVER \u003ca href=\"http://192.168.1.1/dhcplist.html\" title=\"http://192.168.1.1/dhcplist.html\"\u003ehttp://192.168.1.1/dhcplist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eDHCP client configuration entries DHCP CLIENT \u003ca href=\"http://192.168.1.1/dhcpclient.html\" title=\"http://192.168.1.1/dhcpclient.html\"\u003ehttp://192.168.1.1/dhcpclient.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eList of WFQ traffic parameters WFQ \u003ca href=\"http://192.168.1.1/wfqlist.html\" title=\"http://192.168.1.1/wfqlist.html\"\u003ehttp://192.168.1.1/wfqlist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eTraffic conditioning list TRAFFIC CONDITIONING \u003ca href=\"http://192.168.1.1/tclist.html\" title=\"http://192.168.1.1/tclist.html\"\u003ehttp://192.168.1.1/tclist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eList of SNMP parameters SYSTEM \u003ca href=\"http://192.168.1.1/snmplist.html\" title=\"http://192.168.1.1/snmplist.html\"\u003ehttp://192.168.1.1/snmplist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eList of TRAP server entries TRAPS \u003ca href=\"http://192.168.1.1/snmptraplist.html\" title=\"http://192.168.1.1/snmptraplist.html\"\u003ehttp://192.168.1.1/snmptraplist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eList of communities entries COMMUNITIES \u003ca href=\"http://192.168.1.1/snmpcommunitylist.html\" title=\"http://192.168.1.1/snmpcommunitylist.html\"\u003ehttp://192.168.1.1/snmpcommunitylist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eList of IGMP proxy entries IGMP PROXY \u003ca href=\"http://192.168.1.1/igmplist.html\" title=\"http://192.168.1.1/igmplist.html\"\u003ehttp://192.168.1.1/igmplist.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eIGMP Proxy configuration IGMP PROXY \u003ca href=\"http://192.168.1.1/igmpconfig.html\" title=\"http://192.168.1.1/igmpconfig.html\"\u003ehttp://192.168.1.1/igmpconfig.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eSNTP Configuration SNTP \u003ca href=\"http://192.168.1.1/sntp.html\" title=\"http://192.168.1.1/sntp.html\"\u003ehttp://192.168.1.1/sntp.html\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"hidden-features\"\u003eHidden features\u003c/h2\u003e\n\u003ch3 id=\"mac-address-filter\"\u003eMac address filter\u003c/h3\u003e\n\u003cp\u003econnect to the router in telnet :\u003c/p\u003e","title":"Hardware: Comtrend CT-633 hidden things"},{"content":"This article presents the steps to follow in order to use wepcrack (wifi / wep crack).\nNeeded software Before starting, you need the following:\nLinux\u0026rsquo;s wireless tools (iwconfig, etc.) airodump aircrack First Step We first need to configure the wireless card to listen for all packets it can receive:\niwconfig wlan0 mode Monitor wconfig wlan0 channel wpriv wlan0 monitor_type 1 fconfig wlan0 up Second step We now need to dump packet and store them to a file. Be ware that a lot of packets are needed in order to make wep key crack possible.\nairodump wlan0 wlan.pcap Finaly\u0026hellip; We can now use aircrack on the previously generated file (see aircrack usage). For example, to search a 128 bits wep key use a 2 cores cpu:\naircrack -p 2 -n 128 packetsFile.cap ","permalink":"https://www.bluemind.org/linux-wepcrack-quick-howto/","summary":"\u003cp\u003eThis article presents the steps to follow in order to use wepcrack (wifi / wep crack).\u003c/p\u003e\n\u003ch2 id=\"needed-software\"\u003eNeeded software\u003c/h2\u003e\n\u003cp\u003eBefore starting, you need the following:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eLinux\u0026rsquo;s wireless tools (iwconfig, etc.)\u003c/li\u003e\n\u003cli\u003eairodump\u003c/li\u003e\n\u003cli\u003eaircrack\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"first-step\"\u003eFirst Step\u003c/h2\u003e\n\u003cp\u003eWe first need to configure the wireless card to listen for all packets it can receive:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eiwconfig wlan0 mode Monitor\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ewconfig wlan0 channel\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ewpriv wlan0 monitor_type \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efconfig wlan0 up\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"second-step\"\u003eSecond step\u003c/h2\u003e\n\u003cp\u003eWe now need to dump packet and store them to a file. Be ware that a lot of packets are needed in order to make wep key crack possible.\u003c/p\u003e","title":"Linux: wepcrack quick howto"},{"content":"Here is yet another way to produce a little divx file (using mencoder) that don\u0026rsquo;t need to be of an exellent quality (eg. for old mangas episodes):\nmencoder -chapter 1 -dvd-device /dev/cdroms/cdrom0 -o /path/to/myfile.avi -ovc\\ lavc -lavcopts vcodec=mpeg4:vqscale=10 -sws 5 -vop scale=320:240 -oac mp3lame \\ -lameopts cbr:br=128:ratio=5:vol=6 dvd://9 Note: you have to change the dvd-device according to your setting as well as the number after dvd:// to find the track you want to rip\n","permalink":"https://www.bluemind.org/video-convert-a-dvd-chapter-to-divx/","summary":"\u003cp\u003eHere is yet another way to produce a little divx file (using mencoder) that don\u0026rsquo;t need to be of an exellent quality (eg. for old mangas episodes):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emencoder -chapter \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e -dvd-device /dev/cdroms/cdrom0 -o /path/to/myfile.avi -ovc\u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003elavc -lavcopts vcodec\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003empeg4:vqscale\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e10\u003c/span\u003e -sws \u003cspan style=\"color:#ae81ff\"\u003e5\u003c/span\u003e -vop scale\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e320:240 -oac mp3lame \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e-lameopts cbr:br\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e128:ratio\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e5:vol\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e6\u003c/span\u003e dvd://9\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNote: you have to change the dvd-device according to your setting as well as the number after dvd:// to find the track you want to rip\u003c/p\u003e","title":"Video: Convert a DVD chapter to Divx"},{"content":"this article is about convert any video to a file suitable for a sony Clie TH55. You will need ffmpeg to make the following working.\nThe generated video can be watched in full screen on a TH55 with the original sony video player. So this is the DSP which will decode de video thus the battery life is preserved:\nffmpeg -i MyVideoFile.avi -s 320x240 -b 251 -r 23.976 -g 240 -qmin 2 \\ -qmax 15 -acodec mp2 -ab 96 -ar 48000 -vcodec mpeg1video MOV00001.MPG Be carfull, the filename must be like MOVXXXXX.MPG, in upper case, where X is a number\nYou can try other parameters for encoding, but theses ones give a good quality on the TH55 screen (it does not on a computer screen) and it takes about 50 Mb of space for 20 minutes of video.\n","permalink":"https://www.bluemind.org/video-convert-a-video-for-a-clie-th55/","summary":"\u003cp\u003ethis article is about convert any video to a file suitable for a sony Clie TH55. You will need ffmpeg to make the following working.\u003c/p\u003e\n\u003cp\u003eThe generated video can be watched in full screen on a TH55 with the original sony video player. So this is the DSP which will decode de video thus the battery life is preserved:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003effmpeg -i MyVideoFile.avi -s 320x240 -b \u003cspan style=\"color:#ae81ff\"\u003e251\u003c/span\u003e -r 23.976 -g \u003cspan style=\"color:#ae81ff\"\u003e240\u003c/span\u003e -qmin \u003cspan style=\"color:#ae81ff\"\u003e2\u003c/span\u003e  \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e-qmax \u003cspan style=\"color:#ae81ff\"\u003e15\u003c/span\u003e -acodec mp2 -ab \u003cspan style=\"color:#ae81ff\"\u003e96\u003c/span\u003e -ar \u003cspan style=\"color:#ae81ff\"\u003e48000\u003c/span\u003e -vcodec mpeg1video MOV00001.MPG\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBe carfull, the filename \u003cstrong\u003emust be like MOVXXXXX.MPG\u003c/strong\u003e, in upper case, where X is a number\u003c/p\u003e","title":"Video: Convert a video for a Clie TH55"},{"content":"Use XSLT to make a smart pagination rendering result in HTML, more or less like the google one. The following is extracted from an old project of my own and the code is not expected to works as is. It took every needed parts, but as this code is out of its original context, it probably needs some work. Nevertheless, it can be a begining.\nDescription The purpose is to show N pages with \u0026ldquo;\u0026hellip;\u0026rdquo; at the end for the first time results are displayed. Then, if you click on the last shown page, you will see N pages before and N pages after the one you clicked. for example: if you have:\n1 - 2 - 3 - 4 - 5 \u0026hellip; next \u0026raquo;\nclicking on \u0026ldquo;5\u0026rdquo; will display\n\u0026laquo; previous 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 \u0026hellip; next \u0026raquo;/\u0026gt;\nthen clicking on 10 will display\n\u0026laquo; previous \u0026hellip; 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 \u0026hellip; next \u0026raquo;\nXSLT code Handling weither to display \u0026ldquo;\u0026laquo; previous\u0026rdquo; \u0026lt;\u0026lt;previous Handling weither to display \u0026ldquo;\u0026raquo; next\u0026rdquo; next \u0026gt;\u0026gt; Between previous and next link: show page numbers just place a call to the following template (eg. \u0026lt;xsl:call-template name=\u0026ldquo;num_page\u0026rdquo;/\u0026gt;)\n... ... ","permalink":"https://www.bluemind.org/xslt-make-a-smart-pagination-in-html/","summary":"\u003cp\u003eUse XSLT to make a smart pagination rendering result in HTML, more or less like the google one. The following is extracted from an old project of my own and the code is not expected to works as is. It took every needed parts, but as this code is out of its original context, it probably needs some work. Nevertheless, it can be a begining.\u003c/p\u003e\n\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003eThe purpose is to show N pages with \u0026ldquo;\u0026hellip;\u0026rdquo; at the end for the first time results are displayed. Then, if you click on the last shown page, you will see N pages before and N pages after the one you clicked. for example: if you have:\u003c/p\u003e","title":"XSLT: Make a smart pagination in HTML"},{"content":"The following function decode HTML entities en return a plain text string:\nfunction entityDecode(strHTML) { var tmpTextArea = document.createElement(\u0026#34;textarea\u0026#34;); tmpTextArea.innerHTML = strHTML.replace(//g,\u0026#34;\u0026gt;\u0026#34;); var decodedStr = tmpTextArea.value; document.removeElement(tmpTextArea); return decodedStr; } ","permalink":"https://www.bluemind.org/javascript-decode-html-entities/","summary":"\u003cp\u003eThe following function decode HTML entities en return a plain text string:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunction\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eentityDecode\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003estrHTML\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003etmpTextArea\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e document.\u003cspan style=\"color:#a6e22e\"\u003ecreateElement\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;textarea\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#a6e22e\"\u003etmpTextArea\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003einnerHTML\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003estrHTML\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003ereplace\u003c/span\u003e(\u003cspan style=\"color:#75715e\"\u003e//g,\u0026#34;\u0026gt;\u0026#34;);\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edecodedStr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003etmpTextArea\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003evalue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  document.\u003cspan style=\"color:#a6e22e\"\u003eremoveElement\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003etmpTextArea\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edecodedStr\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Javascript: Decode HTML entities"},{"content":"If you want to check if any checkbox is selected, but checkboxes have all the same name and are so part of an array, here is an example of how to do:\nanychecked = false; for(var i=0; i\u0026lt; document.forms.form1[\u0026#34;mycheckboxes\u0026#34;].length;i++) { if(document.forms.form1[\u0026#34;mycheckboxes\u0026#34;][i].checked == true) { anychecked = true; break; } } if(anychecked) alert(\u0026#39;At least one checkbox is checked\u0026#39;); ","permalink":"https://www.bluemind.org/javascript-check-if-any-checkbox-is-checked/","summary":"\u003cp\u003eIf you want to check if any checkbox is selected, but checkboxes have all the same name and are so part of an array, here is an example of how to do:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eanychecked\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ei\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; \u003cspan style=\"color:#a6e22e\"\u003ei\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e document.\u003cspan style=\"color:#a6e22e\"\u003eforms\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eform1\u003c/span\u003e[\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;mycheckboxes\u0026#34;\u003c/span\u003e].\u003cspan style=\"color:#a6e22e\"\u003elength\u003c/span\u003e;\u003cspan style=\"color:#a6e22e\"\u003ei\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e(document.\u003cspan style=\"color:#a6e22e\"\u003eforms\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eform1\u003c/span\u003e[\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;mycheckboxes\u0026#34;\u003c/span\u003e][\u003cspan style=\"color:#a6e22e\"\u003ei\u003c/span\u003e].\u003cspan style=\"color:#a6e22e\"\u003echecked\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eanychecked\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ebreak\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eanychecked\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#a6e22e\"\u003ealert\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;At least one checkbox is checked\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Javascript: Check if any checkbox is checked"},{"content":"ZendFramework provides a class to handle configuration: Zend_Config. It also provides a class to manage cache: Zend_Cache. But, unless the Zend_Translate component (as of the V1.7), Zend_Config does not natively support Zend_Cache. This article explains a solution.\n1 - Classical usage of Zend_config: //[...] $config = new Zend_Config_Ini(\u0026#39;path/to/config.ini\u0026#39;); //[...] limits: For big config files, this is a waste of time as the configuration file has to be parsed for each user\u0026rsquo;s query and application config files seldom changes. Adding a cache mecanism is a good way of optimizing this, but unlike Zend_Translate, Zend_Config does not provides a native support for Zend_Cache.\n2 - Using Zend_Config with Zend_Cache transparently 2.1 problems / expectations caching the config file needs configuration parameters such as lifetime and cache path that cannot be put in configuration file itself =\u0026gt; in this example, we use the session_save_path() as storage, which should be writable by php in most cases using Zend_Cache should be transparent to avoid bootstrap pollution =\u0026gt; in this example we use a special configuration class on which only 1 static method call is needed to get a cached config object. Thus, using it does not add more code than typical Zend_config use as seen above 2.2 The proposed new way Using this example\u0026rsquo;s class will make the code from chapter 1 to become the following:\n//[...] $config = MyApp_Config::getInstance(); //[...] 2.3 The Example class 2.3.1 Source code require_once \u0026#39;Zend/Config/Ini\u0026#39;; require_once \u0026#39;Zend/Cache\u0026#39;; /** * This class is used to retreive the application\u0026#39;s configuration throught a static method. This * way, it is easy to use Zend_Cache to cache the whole configuration which seldom contains changes * @package MyApp */ class MyApp_Config { /** * full path to the configuration file */ const CONFIG_FILE = \u0026#39;settings/application.ini\u0026#39;; /** * Cached instance of the configuration * @var Zend_Cache */ private static $_cache = null; /** * get the application\u0026#39;s configuration * @return Zend_Config */ public static function getConfig() { return new Zend_Config_Ini(self::CONFIG_FILE,null); } /** * get the path for configuration cache (session\u0026#39;s save path) * @return string */ public static function getConfigCachePath() { return session_save_path(); } /** * get an instance of the class (in fact a Zend_Config instance) * @return Zend_Config a cached instance of Zend_Config */ public static function getInstance() { // if never initialized, get a Zend_Cache instance if (!self::$cache) { self::$cache = Zend_Cache::factory(\u0026#39;class\u0026#39;, \u0026#39;file\u0026#39;, array(\u0026#39;cached_entity\u0026#39; =\u0026gt; \u0026#39;MyApp_Config\u0026#39;), array(\u0026#39;cache_dir\u0026#39; =\u0026gt; MyApp_Config::getConfigCachePath())); } return self::$cache-\u0026gt;getConfig(); } /** * clean the cache. A call to getInstance() must have been called once to make this work. */ public static function clearCache() { if (self::$cache) { self::$cache-\u0026gt;clean(); } } } 2.3.2 Remarks This example only works with file based caching (but it should not be difficult to adapt it to use others frontend) As we are caching the application\u0026rsquo;s configuration file itself, it is not really possible to store cache path in a configuration file (unless you would like the cached config system to use another config file itself\u0026hellip; hmmm) This example is suitable for relatively biug config files. If you use little ones, you should do some benchmark to see if it provides any enhancement. I tested this code with a small config file (a dozen of entries) and the footprint was not perceptible using a profiler. So in all cases, this code should not add any overhead. 2.4 Cleanning up the cache In case your config file changed and you don\u0026rsquo;t want to or can\u0026rsquo;t wait for the cache to expires, you can call the clearCache() method in a admin page or a special controller with some access restriction.\n3 Conclusion This was just a little example I wrote with my humble knowledges. It might be a starting point for more complexe configuration file architecture or for other caching mecanism (database, memcached, \u0026hellip;)\nFeel free to send me advices or remarks ;)\n","permalink":"https://www.bluemind.org/php-zendframework-using-zend_config-together-with-zendcache/","summary":"\u003cp\u003eZendFramework provides a class to handle configuration: Zend_Config. It also provides a class to manage cache: Zend_Cache. But, unless the Zend_Translate component (as of the V1.7), Zend_Config does not natively support Zend_Cache. This article explains a solution.\u003c/p\u003e\n\u003ch2 id=\"1---classical-usage-of-zend_config\"\u003e1 - Classical usage of Zend_config:\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e//[...]\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$config \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eZend_Config_Ini\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;path/to/config.ini\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e//[...]\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003elimits:\u003c/strong\u003e\nFor big config files, this is a waste of time as the configuration file has to be parsed for each user\u0026rsquo;s query and application config files seldom changes. Adding a cache mecanism is a good way of optimizing this, but unlike Zend_Translate, Zend_Config does not provides a native support for Zend_Cache.\u003c/p\u003e","title":"PHP / ZendFramework: Using Zend_config together with ZendCache"},{"content":"Acrobat reader throught IE can complain about a \u0026ldquo;document not found\u0026rdquo; when you try to send a PDF throught the web server. A way to avoid this error is to declare the cache-control as private in http header:\nCache-control: private, must-revalidate ","permalink":"https://www.bluemind.org/http-avoid-acrobat-reader-error-with-ie/","summary":"\u003cp\u003eAcrobat reader throught IE can complain about a \u0026ldquo;document not found\u0026rdquo; when you try to send a PDF throught the web server. A way to avoid this error is to declare the cache-control as private in http header:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-http\" data-lang=\"http\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e    Cache-control: private, must-revalidate\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"HTTP: Avoid Acrobat Reader error with IE"},{"content":"A little function to transform an ascii string into a Mac Roman string suitable for MacOS Pre X\nfunction AsciiToMacRoman($str) { return strtr($str, \u0026#34;xc4xc5xc7xc9xd1xd6xdcxe1xe0xe2xe4xe3 xe5xe7xe9xe8xeaxebxedxecxeexefxf1xf3 xf2xf4xf6xf5xfaxf9xfbxfcxb0xa7xb6xdfxae xb4xa8xc6xd8xa5xaaxbaxe6xf8xbfxa1xac xabxbbxa0xc0xc3xf7xffxa4xb7xc2xcaxc1 xcbxc8xcdxcexcfxccxd3xd4xd2xdaxdb xafxb8x22x22x27x27\u0026#34;, \u0026#34;x80x81x82x83x84x85x86x87x88x89x8ax8b x8cx8dx8ex8fx90x91x92x93x94x95x96x97 x98x99x9ax9bx9cx9dx9ex9fxa1xa4xa6xa7 xa8xabxacxaexafxb4xbbxbcxbexbfxc0xc1 xc2xc7xc8xcaxcbxccxd6xd8xdbxe1xe5xe6 xe7xe8xe9xeaxebxecxedxeexefxf1xf2xf3 xf8xfcxd2xd3xd4xd5\u0026#34;); } ","permalink":"https://www.bluemind.org/php-ascii-to-mac-roman/","summary":"\u003cp\u003eA little function to transform an ascii string into a Mac Roman string suitable for MacOS Pre X\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunction\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eAsciiToMacRoman\u003c/span\u003e($str)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003estrtr\u003c/span\u003e($str,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;xc4xc5xc7xc9xd1xd6xdcxe1xe0xe2xe4xe3\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   xe5xe7xe9xe8xeaxebxedxecxeexefxf1xf3\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   xf2xf4xf6xf5xfaxf9xfbxfcxb0xa7xb6xdfxae\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   xb4xa8xc6xd8xa5xaaxbaxe6xf8xbfxa1xac\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   xabxbbxa0xc0xc3xf7xffxa4xb7xc2xcaxc1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   xcbxc8xcdxcexcfxccxd3xd4xd2xdaxdb\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   xafxb8x22x22x27x27\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;x80x81x82x83x84x85x86x87x88x89x8ax8b\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   x8cx8dx8ex8fx90x91x92x93x94x95x96x97\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   x98x99x9ax9bx9cx9dx9ex9fxa1xa4xa6xa7\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   xa8xabxacxaexafxb4xbbxbcxbexbfxc0xc1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   xc2xc7xc8xcaxcbxccxd6xd8xdbxe1xe5xe6\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   xe7xe8xe9xeaxebxecxedxeexefxf1xf2xf3\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e   xf8xfcxd2xd3xd4xd5\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"PHP: Ascii to Mac Roman"},{"content":"Here is a way I found to make full a full text search on entire words only with postgreSQL (tested on v8.1):\nselect field1,field2 from my_table where field1 similar to \u0026#39;[[:\u0026lt;:]]myword[[:\u0026gt;:]]\u0026#39; ","permalink":"https://www.bluemind.org/6/","summary":"\u003cp\u003eHere is a way I found to make full a full text search on \u003cstrong\u003eentire words only\u003c/strong\u003e with postgreSQL (tested on v8.1):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-postgresql\" data-lang=\"postgresql\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eselect\u003c/span\u003e field1,field2 \u003cspan style=\"color:#66d9ef\"\u003efrom\u003c/span\u003e my_table\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003ewhere\u003c/span\u003e field1 \u003cspan style=\"color:#66d9ef\"\u003esimilar\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eto\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;[[:\u0026lt;:]]myword[[:\u0026gt;:]]\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"PostgreSQL: full text search on entire words"},{"content":" BlueMind is a personal technical space about Linux, open source, self-hosting, automation, home automation, hardware and the maker spirit.\nIt is where I document things I build, hack, experiment with, break and eventually get working — from servers and network infrastructure to software, electronics, home automation and Linux systems.\nThe common thread is curiosity and control: understanding how things work, building and adapting them to fit real needs, keeping control of the systems we use, and sharing what was learned along the way.\nFind me elsewhere GitHub YouTube Tumblr Bluesky Discord ","permalink":"https://www.bluemind.org/about/","summary":"\u003csection class=\"about-intro\"\u003e\n  \u003cdiv class=\"about-intro__copy\"\u003e\u003cp\u003eBlueMind is a personal technical space about Linux, open source, self-hosting, automation, home automation, hardware and the maker spirit.\u003c/p\u003e\n\u003cp\u003eIt is where I document things I build, hack, experiment with, break and eventually get working — from servers and network infrastructure to software, electronics, home automation and Linux systems.\u003c/p\u003e\n\u003cp\u003eThe common thread is curiosity and control: understanding how things work, building and adapting them to fit real needs, keeping control of the systems we use, and sharing what was learned along the way.\u003c/p\u003e","title":"About"}]