Industriële fabricage
Industrieel internet der dingen | Industriële materialen | Onderhoud en reparatie van apparatuur | Industriële programmering |
home  MfgRobots >> Industriële fabricage >  >> Manufacturing Technology >> Productieproces

Ultieme Kegerator

Componenten en benodigdheden

SparkFun Arduino Pro Mini 328 - 3.3V/8MHz
× 7
SparkFun Load Cell-versterker - HX711
× 1
Leviton 47603-12B 4x12 telefoondistributiebord
× 1
Raspberry Pi 2 Model B
× 1
AC 100-120V 200-220V 5V 6A 30W Wisselstroomvoeding
× 1
IR-bewegingssensor
× 1
10.6 cu. ft. Diepvriezer
× 1
Slimme digitale postweegschaal
× 5
Rii K12 Ultraslank 2,4 GHz-toetsenbord
× 1
Rasbperry Pi WiFi-dongle
× 1
Arduino DC 5V relaismodule
× 1
DHT11 temperatuur- en vochtigheidssensor (4 pinnen)
× 2
DHT22-temperatuursensor
× 3
HomeBrewStuff roestvrijstalen dubbele tapbiertoren
× 2
Biertorenkoeler
× 1
0-30 PSI I2C-druksensor (3,3 V) ABPMANN030PG2A3
× 1

Benodigde gereedschappen en machines

Houtbewerkingsgereedschap
Hulpmiddelen om de buitenkant van de vriezer aan te passen
Draadknipper
Soldeerbout (algemeen)
Krimpkous

Over dit project

Disclaimer: Ten eerste promoot dit project op geen enkele manier het gebruik of misbruik van alcohol, het is volledig aan de gebruikers welke dranken de inhoud van deze kegerator zullen vormen.

Dit project is ontstaan ​​uit de wens om de inhoud van een kegerator beter te beheren. Een kegerator werkt volgens het basisprincipe van het koud houden van een drank en het koolzuurhoudend houden van de dranken op een bepaalde PSI. Bovendien heb je door jezelf een koud drankje in te schenken geen idee hoeveel er nog in het vat zit. Het zou zonde zijn als er mensen langskomen voor een voetbalwedstrijd op zondag en halverwege de wedstrijd geen bier meer hebben.



Dus de doelen van dit project zijn:

  1. Behoud een constante temperatuur van de dranken, zorg ervoor dat de dranken niet te warm of te koud worden en bevriezen
  2. Zorg ervoor dat een acceptabele hoeveelheid koolzuur op het vat wordt aangebracht om een ​​optimale smaak te behouden
  3. Houd de hoeveelheid drankjes in elk vat bij en geef visuele feedback om ervoor te zorgen dat er voldoende drankjes bij de hand zijn voor het grote spel.
  4. Houd bij hoeveel CO2 er nog in de tank zit die wordt gebruikt om de dranken te carboniseren

De elektronische basiscomponenten en hun gebruik:

  1. Een diepvrieskist wordt gebruikt voor de koeleenheid en als frame om een ​​mooi meubelstuk te maken
  2. Raspberry PI 2 met Windows 10 IoT-kern wordt gebruikt als hersens voor de operatie
  3. Kleine frankeerweegschalen worden gebruikt om het gewicht van elk vat en de CO2-tank te meten. Bij deze frankeerweegschalen is de elektronica verwijderd en zijn een loadcel-versterker en kleine Arduino in de weegschaal ingebouwd. Deze weegschalen communiceren met de Raspberry PI 2 via I2C (hierover later meer)
  4. Er zijn 5 digitale temperatuursensoren die op het apparaat zijn geïnstalleerd, één aan de onderkant van de vrieskist, één bevestigd aan de onderkant van de bovenkant, één elk geïnstalleerd in de torens waar de kraanhandvatten zijn (hierover later meer ) en een aan de buitenkant van het apparaat om de omgevingstemperatuur te meten. Deze temperatuursensoren zijn aangesloten op een kleine Arduino en communiceren ook met de Raspberry PI 2 via I2C
  5. Een Honeywell-druksensor is bevestigd aan de luchtleidingen die worden gebruikt om carbonatatie aan de vaten te leveren. Hoewel de aanpassing van PSI (voorlopig) handmatig is, geeft dit een nauwkeurige meting van de hoeveelheid CO2 die op de vaten wordt toegepast.
  6. Een 5V-voeding wordt gebruikt om de Raspberry PI2 van stroom te voorzien. Er is gekozen voor een grotere versie (die tot 6 ampère levert), zodat deze ook een adresseerbare ledstrip van stroom kan voorzien.
  7. Een eenvoudig relais wordt in lijn geplaatst met de voeding voor de compressor. Met behulp van dit relais kan de stroom naar de compressor worden toegevoerd en verwijderd, de compressor regelt dan op zijn beurt de temperatuur van de kegerator (hierover later meer)

Cloud-connectiviteit

De Ultimate Kegerator bevat een webserver om configuratie op afstand via REST-services mogelijk te maken, evenals een eenvoudige statische weergave van de huidige status. U kunt deze website bereiken op http://slsys.homeip.net:9501 .

Daarnaast uploadt de Ultimate Kegerator zijn vitale statistieken naar een Windows Azure Event Hub. Je kunt het standaard Nuget-pakket niet gebruiken om met de Event Hub te praten, maar je hebt de eenvoudig te implementeren bibliotheek van collega Windows Embedded MVP Paolo Patierno, beschikbaar op

https://www.nuget.org/packages/AzureSBLite/

Voor ultieme verwerking door Stream Analytics

Eventuele plannen voor Stream Analytics zijn:

1) Monitor en informeer als de temperatuur te warm of te koud wordt

2) Bewaken en melden wanneer de CO2-tank te laag wordt

3) Bewaken en melden als er een lek in de CO2-tank wordt gedetecteerd (geleidelijk gewichtsafname)


Hier zijn enkele extra foto's van het montageproces:


-twb

Code

  • Keg-klasse
  • Schaalklasse
  • Kegerator-klasse
Keg ClassC#
Voorbeeld van broncode voordat de volledige broncode op GitHub wordt vrijgegeven. Als u vroege toegang wilt of wilt bijdragen, neem dan contact op met de auteur van dit project
gebruik LagoVista.Common.Commanding;using System;using System.Collections.Generic;using System.Linq;using System.Text;using System .Threading.Tasks;met Windows.UI.Xaml;namespace LagoVista.IoT.Common.Kegerator.Models{ public class Keg:DeviceBase { int _idx; Tijdspanne _updateInterval; privé Weegschalen.Scale _scale; public Keg (int idx, Scales.Scale scale, TimeSpan updateInterval) { _idx =idx; _updateInterval =UpdateInterval; _schaal =schaal; } openbare overschrijving TimeSpan UpdateInterval { get {return _updateInterval; } } public override void Refresh() { LastUpdated =DateTime.Now; LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() => { ContentsWeight =Scale.Weight - ContainerWeightLb; if (FullContentsWeightLb> 0) PercentFull =Convert.ToInt32((ContentsWeight / (FullContents 100WeightLb)) * ContainerWeightLb) else PercentFull =0; PercentFull =Math.Min(PercentFull, 100); if (GlassSizeOz> 0) QtyRemaining =Convert.ToInt32((ContentsWeight * 16)/ GlassSizeOz); else QtyRemaining =0; RaisePropertyChanged("PercentFullProperHeight"); ("PercentFullDisplay"); }); } public Scales.Scale Scale {get {return _scale; } } #region Berekende eigenschappen private int _qtyRemaining; public int QtyRemaining { get { return _qtyRemaining; } set { Set (ref _qtyRemaining, waarde); } } privé DateTime? _installatiedatum; openbare DatumTijd? InstallDate { get { return _installDate; } set { Set (ref _installDate, waarde); } } privé int _percentFull; public int PercentFull { get {return _percentFull; } set { Set (ref _percentFull, waarde); } } public String PercentFullDisplay { get { return String.Format("{0}%", Convert.ToInt32(PercentFull)); } } public double PercentFullHeight { get { return Convert.ToDouble (_percentFull * 2); } } public int KegIndex { get { return _idx; } } #endregion #region Ingevoerde eigenschappen private bool _isEmpty; public bool IsEmpty { get {return _isEmpty; } set { _isEmpty =waarde; RaisePropertyChanged(); } } privé dubbele _glassSize; openbare dubbele GlassSizeOz { get {return _glassSize; } set { Set (ref _glassSize, waarde); } } privé DateTime? _geboortedatum; openbare DatumTijd? BornDate { get {return _bornDate; } set { Set (ref _bornDate, waarde); } } dubbele _containerWeight; openbare dubbele ContainerWeightLb { get {return _containerWeight; } set { Set (ref _containerWeight, waarde); } } dubbele _contentsWeight; openbare dubbele ContentsWeight { get { return _contentsWeight; } set { Set (ref _contentsWeight, waarde); } } dubbel _fullContentsWeight; openbare dubbele FullContentsWeightLb { get {return _fullContentsWeight; } set { Set (ref _fullContentsWeight, waarde); } } privé String _contentsName; public String ContentsName { get { return _contentsName; } set { Set (ref _contentsName, waarde); } } #endregion public void Save() { LagoVista.Common.PlatformSupport.Services.BindingHelper.RefreshBindings(); PutSetting(String.Format("KEG{0}_CONTENTS", _idx), ContentsName); PutSetting(String.Format("KEG{0}_IS_EMPTY", _idx), IsEmpty.ToString()); PutSetting(String.Format("KEG{0}_CONTAINER_WEIGHT", _idx), String.Format("{0:0.00}", ContainerWeightLb)); PutSetting(String.Format("KEG{0}_GLASS_SIZE", _idx), String.Format("{0:0.00}", GlassSizeOz)); PutSetting(String.Format("KEG{0}_FULL_CONTENTS_WEIGHT", _idx), String.Format("{0:0.00}", FullContentsWeightLb)); if (BornDate.HasValue) PutSetting(String.Format("KEG{0}_BORN_DATE", _idx), BornDate.Value.ToString()); else RemoveSetting(String.Format("KEG{0}_BORN_DATE", _idx)); if(InstallDate.HasValue) PutSetting(String.Format("KEG{0}_INSTALL_DATE", _idx), InstallDate.Value.ToString()); else RemoveSetting(String.Format("KEG{0}_INSTALL_DATE", _idx)); } public void Load() { ContentsName =GetSetting(String.Format("KEG{0}_CONTENTS", _idx), "?"); ContainerWeightLb =Convert.ToDouble(GetSetting(String.Format("KEG{0}_CONTAINER_WEIGHT", _idx), "10.0")); GlassSizeOz =Convert.ToDouble(GetSetting(String.Format("KEG{0}_GLASS_SIZE", _idx), "12.0")); FullContentsWeightLb =Convert.ToDouble(GetSetting(String.Format("KEG{0}_FULL_CONTENTS_WEIGHT", _idx), "0.0")); IsEmpty =Convert.ToBoolean(GetSetting(String.Format("KEG{0}_IS_EMPTY", _idx), "True")); var bornDate =GetSetting("KEG{0}_BORN_DATE", String.Empty); if (!String.IsNullOrEmpty(bornDate)) BornDate =DateTime.Parse(bornDate); anders Geboortedatum =null; var installDate =GetSetting("KEG{0}_INSTALL_DATE", String.Empty); if (!String.IsNullOrEmpty(installDate)) InstallDate =DateTime.Parse(installDate); anders Installatiedatum =null; } public async void SaveFullWeight() { FullContentsWeightLb =wacht op Scale.GetAverageWeight(); Sparen(); } public RelayCommand SaveFullWeightCommand { get {return nieuwe RelayCommand(() => SaveFullWeight()); } } }}
SchaalklasseC#
Voorbeeld van broncode voordat de volledige broncode op GitHub wordt vrijgegeven. Als u vroege toegang wilt of wilt bijdragen, neem dan contact op met de auteur van dit project
gebruik LagoVista.Common.Commanding;using System;using System.Collections.Generic;using System.Diagnostics;using System.Linq;using System .Text;met System.Threading.Tasks;met Windows.Devices.I2c;namespace LagoVista.IoT.Common.Kegerator.Scales{ openbare klasse Schaal:DeviceBase {Windows.Devices.I2c.I2cDevice _scaleI2CChannel; int _countOffset; dubbele? _calibrationFactor =null; privé TimeSpan _updateInterval; byte _adres; openbare schaal (byte-adres) { _address =adres; } private void WriteValue (byte-adres, int-waarde) {if (!IsDemoMode) {var offsetBuffer =nieuwe byte [5]; offsetBuffer[0] =adres; offsetBuffer [1] =(byte) (waarde>> 24); offsetBuffer [2] =(byte) (waarde>> 16); offsetBuffer [3] =(byte) (waarde>> 8); offsetBuffer [4] =(byte)(waarde); _scaleI2CChannel.Write (offsetBuffer); } } public async Task Init (String i2cDeviceId, TimeSpan updateInterval) {var settings =new I2cConnectionSettings(_address) {BusSpeed ​​=I2cBusSpeed.StandardMode, SharingMode =I2cSharingMode.Shared}; _updateInterval =updateInterval; IsDemoMode =String.IsNullOrEmpty(i2cDeviceId); if (!IsDemoMode) { _scaleI2CChannel =wacht op Windows.Devices.I2c.I2cDevice.FromIdAsync(i2cDeviceId, instellingen); if (Windows.Storage.ApplicationData.Current.LocalSettings.Values.ContainsKey(String.Format("{0:X}.OFFSET", _address))) { _countOffset =Convert.ToInt32(Windows.Storage.ApplicationData.Current.LocalSettings .Waarden[String.Format("{0:X}.OFFSET", _adres)]); probeer {WriteValue((byte)'O', _countOffset); } catch (uitzondering ex) { Debug.WriteLine("Schaal offline "); } } if (Windows.Storage.ApplicationData.Current.LocalSettings.Values.ContainsKey(String.Format("{0:X}.CALIBRATION", _address))) { _calibrationFactor =Convert.ToDouble(Windows.Storage.ApplicationData.Current .LocalSettings.Values[String.Format("{0:X}.CALIBRATION", _address)]); LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() => { Status ="Klaar"; }); } } else { LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() => { Status ="Klaar"; }); } } int? _lastRaw =null; private int GetRaw() { probeer { var inbuffer =nieuwe byte [4]; _scaleI2CChannel.Write (nieuwe byte [] { (byte) 0x11 }); _scaleI2CChannel.Read (inbuffer); /* Let op de schaal, dit is een lange (64 bit) hier is het een int (64 bit) */ var thisRaw =(int)(inbuffer[0] <<24 | inbuffer[1] <<16 | inbuffer[ 2] <<8 | inbuffer[3]); if (_lastRaw.HasValue) {if (Math.Abs(_lastRaw.Value - thisRaw)> 0xFFFF) retourneer _lastRaw.Value; } else _lastRaw =dezeRaw; retourneer ditRaw; } catch (uitzondering) { return -1; } } public override void Refresh() { LastUpdated =DateTime.Now; int rawResult =0; var isOnline =waar; probeer { var inbuffer =nieuwe byte [4]; var statusBuffer =nieuwe byte [1]; if (!IsDemoMode) { _scaleI2CChannel.Write(nieuwe byte [] { (byte)0x0A}); _scaleI2CChannel.Read (statusBuffer); rawResult =GetRaw(); } if (_calibrationFactor.HasValue) { Gewicht =(rawResult - _countOffset) * _calibrationFactor.Value; Debug.WriteLine(String.Format("0x{0:X} WEIGHT VALUE => {1:0.00} lbs", _address, Weight)); } else if (_countOffset> 0) Debug.WriteLine(String.Format("0x{0:X} ZEROED VALUE => {1}", _address, rawResult - _countOffset)); else Debug.WriteLine(String.Format("0x{0:X} RAW VALUE => 0x{1:X}", _address, rawResult)); } catch (uitzondering ex) { rawResult =-1; isOnline =onwaar; Debug.WriteLine(ex.Message); } LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() => { Raw =rawResult; IsOnline =isOnline; if (!IsOnline) { Status ="Offline"; WeightDisplay ="?"; } else { if (_calibrationFactor .HasValue) { Status ="Ready"; WeightDisplay =String.Format("{0}lb {1:00}oz", Math.Truncate(Weight), ((Gewicht % 1.0) * 16.0));} else { WeightDisplay ="?"; Status ="Niet gekalibreerd"; } } RaisePropertyChanged("LastUpdateDisplay"); }); } const int CALIBRATION_COUNT =10; public async void StoreOffset() { LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() => { Status ="Zeroing"; }); Debug.WriteLine("Nulproces starten"); lange nulsom =0; for (var idx =0; idx  { Status ="Nul"; }); } public async void Calibrate() { Status ="Kalibratie"; LagoVista.Common.PlatformSupport.Services.BindingHelper.RefreshBindings(); lange countSum =0; for (var idx =0; idx  GetAverageWeight(int pointCount =5) { var weightSum =0.0; for(var idx =0; idx  StoreOffset ()); } } public RelayCommand CalibrationCommand { get {return new RelayCommand(() => Calibrate()); } } }}
Kegerator-klasseC#
Voorbeeld van broncode voordat de volledige broncode op GitHub wordt vrijgegeven. Als u vroege toegang wilt of wilt bijdragen, neem dan contact op met de auteur van dit project
gebruik LagoVista.Common.Commanding;using System;using System.Collections.Generic;using System.Collections.ObjectModel;gebruik System.ComponentModel; met behulp van System.Linq;met behulp van System.Runtime.CompilerServices;met behulp van System.Text;met behulp van System.Threading.Tasks;met behulp van Windows.Devices.Enumeration;met behulp van Windows.Devices.I2c;namespace LagoVista.IoT.Common.Kegerator{ openbare klasse Kegerator :INotifyPropertyChanged { openbare gebeurtenis PropertyChangedEventHandler PropertyChanged; privémodellen.Keg _keg1; privé modellen.Keg _keg2; privémodellen.Keg _keg3; privémodellen.Keg _keg4; privé CO2.CO2Tank _co2Tank; private Kegerator() {} openbare lijst _devices =nieuwe lijst(); private void RaisePropertyChanged([CallerMemberName] string propertyName =null) { var eventHandler =this.PropertyChanged; if (eventHandler! =null) { eventHandler (this, new PropertyChangedEventArgs (propertyName)); } } private bool Set(ref T storage, T value, string columnName =null, [CallerMemberName] string propertyName =null) { if (object.Equals(storage, value)) return false; opslag =waarde; dit.RaisePropertyChanged(propertyName); retourneer waar; } byte [] _scalesAddresses ={ 0x43, 0x41, 0x40, 0x42 }; private const string I2C_CONTROLLER_NAME ="I2C1"; privé Thermo.Temperaturen _temperaturen; privé Thermo.Controller _tempController; privé Weegschalen.Scale _co2Scale; privé Woordenboek _kegScales; privé CO2.DrukSensor _drukSensor; privé LED.LEDManager _ledManager; privé REST.KegeratorServices _kegServices; privé statisch Kegerator _kegerator =nieuwe Kegerator(); openbare statische Kegerator-instantie {get {return _kegerator; } } privé CloudServices.EventHubClient _eventHubClient; Systeem.Threading.Timer _timer; private bool _initialized =false; public async Task Init() { if (!_initialized) { _initialized =true; var selector =I2cDevice.GetDeviceSelector(I2C_CONTROLLER_NAME); /* Zoek de selectorstring voor de I2C-buscontroller */ var deviceInfo =(wacht op DeviceInformation.FindAllAsync(selector)).FirstOrDefault(); /* Zoek het I2C-buscontrollerapparaat met onze selectorstring */ var deviceId =deviceInfo ==null ? (string)null :deviceInfo.Id; _temperatures =nieuwe Thermo.Temperaturen (0x48); wacht op _temperatures.Init(deviceId); _devices.Toevoegen(_temperaturen); _tempController =nieuwe Thermo.Controller(); _tempController.Init(_temperaturen); _devices.Add(_tempController); _pressureSensor =nieuwe CO2.PressureSensor(); wacht op _pressureSensor.Init(deviceId, TimeSpan.FromSeconds(1)); _devices.Add(_druksensor); _co2Scale =nieuwe Scales.Scale (0x44); wacht op _co2Scale.Init(deviceId, TimeSpan.FromSeconds(1)); _devices.Toevoegen(_co2Scale); _co2Tank =nieuwe CO2.CO2Tank(_co2Scale, TimeSpan.FromSeconds(2)); _co2Tank.Load(); _devices.Toevoegen(_co2Tank); _kegScales =nieuwe Dictionary(); _eventHubClient =nieuwe CloudServices.EventHubClient(this, TimeSpan.FromSeconds(2)); _devices.Add(_eventHubClient); for (var idx =0; idx <4; ++idx) { var scale =new Scales.Scale(_scalesAddresses[idx]); wachten scale.Init(deviceId, TimeSpan.FromMilliseconds(500)); _kegScales.Add(idx, schaal); _devices.Toevoegen (schaal); } _keg1 =nieuwe modellen.Keg(1, _kegScales[0], TimeSpan.FromMilliseconds(500)); _keg1.Laden(); _devices.Toevoegen(_keg1); _keg2 =nieuwe modellen.Keg(2, _kegScales[1], TimeSpan.FromMilliseconds(500)); _keg2.Laden(); _devices.Toevoegen(_keg2); _keg3 =nieuwe modellen.Keg(3, _kegScales[2], TimeSpan.FromMilliseconds(500)); _keg3.Load(); _devices.Toevoegen(_keg3); _keg4 =nieuwe modellen.Keg(4, _kegScales[3], TimeSpan.FromMilliseconds(500)); _keg4.Load(); _devices.Toevoegen(_keg4); DateInitialized =DateTime.Now.ToString(); Web.WebServer.Instance.StartServer(); _kegServices =nieuwe REST.KegeratorServices() {Poort =9500}; _kegServices.EventContent +=_kegServices_EventContent; _kegServices.StartServer(); _timer =nieuw System.Threading.Timer((status) => { Refresh(); }, null, 0, 250); } } private void _kegServices_EventContent(object afzender, string e) { var parts =e.Split('/'); if (parts.Count()> 0) { switch (parts[1]) { case "zero":{ var scaleIndex =Convert.ToInt32(parts[2]); _kegScales[scaleIndex].StoreOffset(); } pauze; case "cal":{ var scaleIndex =Convert.ToInt32(parts[2]); _kegScales[scaleIndex].CalibrationWeight =Convert.ToDouble(parts[3]); _kegScales[scaleIndex].Kalibreer(); } pauze; } } } public void Refresh() { foreach (var device in _devices) { if (DateTime.Now> (device.LastUpdated + device.UpdateInterval)) device.Refresh(); } LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() => { CurrentTimeDisplay =DateTime.Now.ToString(); RaisePropertyChanged("CurrentTimeDisplay");}); } openbare Thermo.Temperatures Temperatures { get { return _temperatures; } } openbare Thermo.Controller TemperatureController { get { return _tempController; } } privé String _statusMessage; public String StatusMessage {get {return _statusMessage; } set { Set (ref _statusMessage, waarde); } } public List KegScales { get { return _kegScales.Values.ToList(); } } public void ToggleCompressor() { if (_tempController.IsCompressorOn) _tempController.CompressorOff(); else _tempController.CompressorOn(); } public String DateInitialized { get; set; } public String CurrentTimeDisplay { get; set; } public Scales.Scale CO2Scale {get {return _co2Scale; } } openbare CO2.PressureSensor PressureSensor { get { return _pressureSensor; } } public Models.Keg Keg1 { get { return _keg1; } } public Models.Keg Keg2 { get { return _keg2; } } public Models.Keg Keg3 { get { return _keg3; } } public Models.Keg Keg4 { get { return _keg4; } } openbare CO2.CO2Tank CO2Tank { get {retour _co2Tank; } } public RelayCommand ToggleCompressorCommand { get {return nieuwe RelayCommand (ToggleCompressor); } } }}

Schema's

Schema systeemcomponenten op hoog niveau

Productieproces

  1. Obstakel vermijden met behulp van kunstmatige intelligentie
  2. Gyroscoopplezier met NeoPixel Ring
  3. Arduino-gamecontroller
  4. Cloud-kostuum
  5. Industriële lijnvolger voor het leveren van materialen
  6. Pixie:een op Arduino gebaseerd NeoPixel-polshorloge
  7. Arduino-aangedreven waterfles
  8. Holiday Shadow Theater
  9. Mobiele bewakingscamera op afstand
  10. Smart Hand Glove
  11. Ultieme draadvonkmachine in Taiwan