Projectile Simulator

Ein ambitioniertes Python-Projekt.

# improvement todos
# + use faster OCR
# + find ground line
#   - and all tanks on it
# + simulate underground path
# + simulate bounding (game edges
#   - and pink obstacles)
# - auto-scale based on game window size
# - calculate high and low hit paths automatically
# - simulate bouncing weps (only on flat ground yet)
# OR
# - based on current shot settings, adapt to hit nearest enemy tank
BENCH_OCR = False#True
OCRS = ["paddleocr", "winrt", "tesseract"]
NO_PP = True # paddle is very robust and doesn't need it, preprocessing can fail, too
import math
import sys
import re
import getopt
import os
import time
from timeit import default_timer as timer
import threading
from tkinter import *
# ocr
# keras won't run and conflicts with paddle
if "doctr" in OCRS: # very slow: >4s
    from doctr.models import ocr_predictor # pip install python-doctr
    model = ocr_predictor(pretrained=True)
    from doctr.io import DocumentFile
if "tesseract" in OCRS: # slow: >200ms
    import pytesseract # pip install pytesseract
    pytesseract.pytesseract.tesseract_cmd = r'C:\Storage\Tools\Tesseract-OCR\tesseract.exe' # xxx edit
if "winrt" in OCRS: # very fast: 12ms
    from screen_ocr import Reader # pip install screen-ocr[winrt]
    ocr_reader = Reader.create_quality_reader()
if "easyocr" in OCRS: # slow: >300ms
    import easyocr # pip install easyocr
    easy_reader = easyocr.Reader(['en'])
if "paddleocr" in OCRS: # fast and accurate: 60ms
    from paddleocr import PaddleOCR # pip install paddlepaddle, paddleocr
    from numpy import asarray
    paddle_ocr = PaddleOCR(lang='en', show_log=False)
    import logging
    from paddleocr.ppocr.utils.logging import get_logger as ppocr_get_logger
    ppocr_get_logger().setLevel(logging.ERROR)
from PIL import ImageTk
import PIL.ImageOps
# screenshot
import pyautogui
# windows
import ctypes
from ctypes.wintypes import HWND, DWORD, RECT
def GetWindowRectFromName(name:str)-> tuple:
    hwnd = ctypes.windll.user32.FindWindowW(0, name)
    rect = ctypes.wintypes.RECT()
    try:
        ctypes.windll.user32.GetWindowRect(hwnd, ctypes.pointer(rect))
        return (rect.left+9, rect.top, rect.right-8, rect.bottom-11)
    except:
        return (0,0,1882,1421)
gcoord = GetWindowRectFromName('ShellShock Live')
# Options
xoffs,yoffs,x2,y2 = gcoord
width = x2-xoffs
height = y2-yoffs
verbose = False
auto = True # auto now default on
def printhelp():
    print(os.path.basename(__file__)+' [-x OFFSET] [-y OFFSET] [-v|--verbose] [-a|--noauto]')
    # print("MODES:\n - 0|1: default trajectory\n - 2: slow projectile\n - 3: hover ball\n - 4: straight\n - 5: 3d bomb")
try:
    opts, args = getopt.getopt(sys.argv[1:], "hax:y:v", ["verbose","auto"])
except getopt.GetoptError:
    printhelp()
    sys.exit()
# print(opts)
for opt, arg in opts:
    if opt == '-h':
        printhelp()
        sys.exit()
    elif opt in ("-x"):
        xoffs = int(arg)
        width = 1882 # fixed to yoga window size
    elif opt in ("-y"):
        yoffs = int(arg)
        height = 1421 # fixed
    elif opt in ("-v", "--verbose"):
        verbose = True
    elif opt in ("-a", "--noauto"):
        auto = False
# Config
GUI_SIZE = "950x800" # init value, size changes are stored to cfg file
WINDOW_OFFS = (xoffs, yoffs) # for moved window
IMG_SCALE = 2.0 # scale output down
SHOW_CROPS = verbose # show cropped img for wind and pa
GAME_CRD = (WINDOW_OFFS[0], WINDOW_OFFS[1], width+WINDOW_OFFS[0], height+WINDOW_OFFS[1]) # game window
# size-dependent
# todo find on scaled window
PLAY_CRD = (5, 180, width-5, height-(1421-1270)) # playing field
WEP_CRD = (700-5, 1375, 999, 1405) # weapon name
FIRE_CRD = (1070, 1300, 1070+1, 1300+1) # a pixel in fire button
WIND_CRD = (924, 143, 958, 167) # wind speed - this is super sensitive to window x displacements!!!
WINDR_X = 979 # wind dir indicators
WINDL_X = 902
WIND_Y = 152
TANKY_OFFS = 2 # center of shot offs to found tank center
BARREL_LENGTH = 21.0
BARREL_LENGTH_Y = 21.0 # todo get rid of different y
BOUNDER_HEIGHT = 218 # side-bounders
A_GND_RANGE = 20 # pixel to scan left&right to find gnd angle
# unscaled values
BOMB_T = 194
HOVER_T = 155 # ca. 0.8s
MAX_T = 2000 # max. flight simulation time ca 10s
BOOMERANG_F = 486.0 # pseudo wind factor for boomerang weps
RAM_F = 4.9 # grav increase on apex for battering weps
BOUNCE_FACTOR = (0.31,0.51,0.71,0.81) # reduction of bouncing wep speed, (normal, med, high, ultra-bounce); bouncing is experimental - stone-decoration makes ground-detection imprecise, bouncing is only correct on flat ground
# ballistic parameters
SF = 0.04885#0.04222 # speed factor
WF = 0.0000128#sf/8.12*power/100.0 # wind acc factor
GACC = 9.803/1000.0 # earth acceleration
SCAN_WEP = True # OCR wep name
COLOR = [(255, 255, 255), (200, 200, 255)] # trajectory color right;left
AUTO = auto
PRINT = not auto
# read weapon list
weps = []
try:
    for l in open("weps.txt").readlines():
        weps.append(l.strip())
except:
    print("Weapon list wep.txt not found")
WEAPONS = weps
#Create an instance of tkinter frame
win = Tk()
win.title("Shellshock Simulator")
#Set the geometry of tkinter frame
CONFIG_FILE = os.path.basename(__file__)+".conf"
if os.path.isfile(CONFIG_FILE):
    #Here I read the X and Y positon of the window from when I last closed it.
    with open(CONFIG_FILE, "r") as conf:
        win.geometry(conf.read())
else:
    #Default window position.
    win.geometry(GUI_SIZE)
wind = StringVar()
power = StringVar()
angle = StringVar()
bfactor = DoubleVar()
bfactor.set(0.3)
subwep = IntVar()
bounce = IntVar()
bound = IntVar()
autowep = IntVar()
override = IntVar()
md = IntVar() # mode
md.set(0)
autowep.set(1)
sfv = DoubleVar()
sfv.set(SF)
wfv = DoubleVar()
wfv.set(WF)
gaccv = DoubleVar()
gaccv.set(GACC)
def ocr_winrt(im):
    return ocr_reader.read_image(im).as_string()
def ocr_tesser(img, cfg='--psm 7 -c tessedit_char_whitelist=0123456789-,'):
    return pytesseract.image_to_string(img, config=cfg)
def ocr_easy(im):
    im.save('temp.png')
    result = easy_reader.readtext('temp.png')
    r = ""
    for (bbox, text, prob) in result:
        r += text
    return r
def ocr_paddle(im):
    numpydata = asarray(im)
    result = paddle_ocr.ocr(numpydata)
    r = ""
    for idx in range(len(result)):
        res = result[idx]
        try:
            for line in res:
                r += line[1][0]
        except:
            pass#print(res) # reader failed; returned None
    return r
def ocr_doctr(im):
    im.save('temp.png')
    doc = DocumentFile.from_images("temp.png")
    result = model(doc)
    print(result)
    return ""
def ocr(im, method=OCRS[0], cfg=''):
    if method == "paddleocr":
        return ocr_paddle(im)
    elif method == "winrt":
        return ocr_winrt(im)
    elif method == "tesseract":
        return ocr_tesser(im, cfg)
    elif method == "easyocr":
        return ocr_easy(im)
    elif method == "doctr":
        return ocr_doctr(im)
    return ""
def distance(p1, p2):
    return math.sqrt((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)
def preprocess(img):
    if NO_PP:
        return img
    size_x, size_y = img.size
    img = img.resize((size_x*2, size_y*2), 3)
    # img = PIL.ImageOps.invert(img)
    # if SHOW_CROPS:
        # img.show()
    # img = img.resize(((pow_rect[2]-pow_rect[0])*2, (pow_rect[3]-pow_rect[1])*2), Image.ANTIALIAS)
    pixdata = img.load()
    d = 16
    for y in range(0, img.size[1]):
        for x in range(0, img.size[0]):
            r, g, b = pixdata[x, y]
            #splitp = 120
            #repl = 200
            #if (r in range(0, splitp)) or (g in range(0, splitp)) or (b in range(0, splitp)):
            # if (r in range(splitp,255)) and (g in range(splitp,255)) and (b in range(splitp,255)):
            if r in range(b-d, b+d) and g in range(b-d, b+d):# and g>150:# (r in range(0, splitp)) or (g in range(0, splitp)) or (b in range(0, splitp)):#b > g*1.5 and r > g*1.5:#g > 160:#
                pixdata[x, y] = (b//2, b//2, b//2)
            else:
                pixdata[x, y] = (0, 0, 0)
            #~ if (r in range(splitp, 256)) and (g in range(splitp, 256)) and (b in range(splitp, 256)):
                #~ pixdata[x, y] = (255, 255, 255)
    return PIL.ImageOps.invert(img)
def postprocess(pa):
    try:
        p, a = re.split('[^0123456789-]+', pa.strip())
        if re.search(r"\d+", p.strip()) and re.search(r"\d+", a.strip()): # check if output is 2 numbers
            return (p, a)
    except:
        pass
    return ""
img_pa = ""
def get_pow_ang(im, pow_rect):
    global img_pa
    img = im.crop(pow_rect)#.convert('L')#.resize()#.invert()
    img = preprocess(img)
    if SHOW_CROPS:
        img.show()
    img_pa = ImageTk.PhotoImage(img)
    pa = ocr(img)
    return pa
    try:
        p, a = re.split('[^0123456789-]+', pa.strip())
        if re.search(r"\d+", p.strip()) and re.search(r"\d+", a.strip()): # check if output is 2 numbers
            return pa
    except:
        pass
    return ""
def get_pow_ang_t(im, pow_rect, ocr_method = "tesseract"):
    global img_pa
    img = im.crop(pow_rect)
    img = preprocess(img)
    if SHOW_CROPS:
        img.show()
    img_pa = ImageTk.PhotoImage(img)
    pow_ang = ocr(img, ocr_method)#pytesseract.image_to_string(img, config='--psm 7 -c tessedit_char_whitelist=0123456789-,')
    return pow_ang
def find_pa(im, tx, ty):
    im_ = im.load()
    limit = 210
    px = []
    for x in range(tx+30, tx+130):
        try:
            r,g,b = im_[x, ty+5]
        except:
            r,g,b = 0,0,0
        if r>limit and g>limit and b>limit:
            px.append(x)
    if len(px) > 2: # found on right side
        return (min(px)-10, ty-10, max(px)+10, ty+30)
    for x in range(tx-130, tx-30):
        try:
            r,g,b = im_[x, ty+5]
        except:
            r,g,b = 0,0,0
        if r>limit and g>limit and b>limit:
            px.append(x)
    if len(px) > 2: # found on left side
        return (min(px)-10, ty-10, max(px)+10, ty+30)
    return (tx-64, ty+25, tx+64, ty+70) # extended x bc on edges the display jumps a bit to the middle
img_wind = ""
def get_wind(im, wind_rect):
    global img_wind
    img = im.crop(wind_rect)
    if SHOW_CROPS:
        img.show()
    img_wind = ImageTk.PhotoImage(img)
    dircheck = im.load()
    splitp = 90
    cnt = 0
    sign = 0
    # print('sign:')
    # print(dircheck[WINDL_X, WIND_Y])
    r, g, b = dircheck[WINDL_X, WIND_Y]
    if (r in range(0, splitp)) or (g in range(0, splitp)) or (b in range(0, splitp)): # black -> no arrow
        sign = 1
        cnt += 1
    # print(dircheck[WINDR_X, WIND_Y])
    r, g, b = dircheck[WINDR_X, WIND_Y]
    if (r in range(0, splitp)) or (g in range(0, splitp)) or (b in range(0, splitp)):
        sign = -1
        cnt += 1
    if cnt == 2: # no arrow seen
        if PRINT:
            print("no wind")
        return 0
    for o in OCRS:
        w = ocr(img, o)
        try:
            if w != "":
                w = int(w)
                return sign*int(w)
        except:
            pass
    if PRINT:
        print("failed to read wind")
    return 0
def calc_dir(gnd, x, wx):
    dir = 0
    try:
        if gnd[wx] < gnd[x]: # smaller means up
            dir = 1
        if gnd[wx] > gnd[x]:
            dir = -1
    except:
        pass
    return dir
def calc_gnd_angle(gnd, x):
    wx = x
    if x > 1: # walk backwards for N pixels as long as they are monotonous
        wx = x-1
        dir = calc_dir(gnd, x, wx)
        while wx>1 and x-wx < A_GND_RANGE and calc_dir(gnd, wx, wx-1) in [0,dir]:
            wx -= 1
    leftx = wx
    wx = x
    if x < len(gnd)-1: # walk forwards for N pixels as long as they are monotonous
        wx = x+1
        dir = calc_dir(gnd, x, wx)
        while wx<len(gnd)-1 and wx-x < A_GND_RANGE and calc_dir(gnd, wx, wx+1) in [0,dir]:
            wx += 1
    rightx = wx
    dx = rightx - leftx
    dy = gnd[rightx] - gnd[leftx]
    a = math.atan2(-dy, dx)
    if a >= math.pi/2.0:
        a -= math.pi
    return (a, dx, dy)
def calc_refl_angle(a, v0x, v0y):
    v1x = v0x*math.cos(2*a) + v0y*math.sin(2*a)
    v1y = v0x*math.sin(2*a) - v0y*math.cos(2*a)
    return (v1x, v1y)
img = "" # keep img reference alive
def sim_shot(im, x, y, power, angle, wind, weapon, gnd):
    global img
    slow_weps = ('Baby Seagull', 'Mama Seagull', 'Seagull', 'BFG-1000', 'BFG-9000', 'Chopper', 'Apache')
    straight_weps = ('AK-47', 'M4', 'M16', 'Glock', 'M9', 'Desert Eagle', 'UZI', 'MP5', 'P90', 'Laser Beam', 'Great Beam', 'Ultra Beam', 'Master Beam', 'Flintlock', 'Blunderbuss', 'Fat Stacks', 'Dolla Bills', 'Make-it-Rain')
    submersive_weps = ('Sub-Sniper', 'Tunneler', 'Mega-Tunneler', 'Torpedoes', '3D-Bomb', 'Rainbow', 'MegaRainbow', 'Dead Riser', 'Yin Yang', 'Yin Yang Yong', 'Penetrator', 'Penetrator v2', 'Sausage', 'Bratwurst', 'Kielbasa')
    noprojectile_weps = ('Earthquake', 'Mega-Quake', 'Shank', 'Dagger', 'Sword', 'Attractoids', 'Shockwave', 'Sonic Pulse', 'Rampage', 'Riot', 'Imploder', 'Ultimate Imploder', 'ShockShell', 'ShockShell Trio')
    hover_wep = ('Hover-Ball', 'Heavy Hover-Ball')
    drei_d_wep = ('3D-Bomb', '2x3D', '3x3D') # wind affects those, too!
    boomerang_wep = ('Boomerang', 'BigBoomerang')
    taser_wep = ('Taser', 'Heavy Taser')
    ram_wep = ('Battering Ram', 'Double Ram', 'Ram-Squad', 'Double Ram-Squad')
    ultra_bounce_wep = ('Snowball','Bounsplode','Double-Bounsplode','Triple-Bounsplode')
    highbounce_wep = ('One-Bounce', 'Three-Bounce', 'Five-Bounce', 'Seven-Bounce','Stone','Boulder','Fireball')
    medium_bounce_wep = ('Grenade', 'Tri-Nade', 'Multi-Nade','Sillyballs','Wackyballs','Crazyballs','Beacon','Beaconator','Jack-o-Lantern','Jack-o-Vomit')
    bounce_wep = ('Grenade Storm', 'Cactus Strike', 'Tunnel Strike', 'Air Strike','Helicopter Strike','AC-130','Artillery', 'Bolt','Lightning','2012','Disco Ball','Groovy Ball','Water Balloon','Water Trio','Water Fight','Pinata','Fiesta','Mine Layer','Sticky Rain',
                  'Rain','Hail','FireStorm','Shrapnel','Shredders','Carpet Bomb','Carpet Fire','Incendiary Bombs','Snowstorm','Bounstream','Bounstrike','Shooting Star','Kamikaze','Suicide Bomber',
                  'Acid Rain','Acid Hail','Recruiter','Enroller','Enlister','Cats and Dogs','Satellite','UFO','Kernels','Popcorn','Burnt Popcorn','Pinpoint','Needles','Pins and Needles',
                  'God Rays','Deity')
    if PRINT:
        print("SIM Power & Angle: ", power, angle)
        print("SIM Wind: ", wind)
    if weapon in noprojectile_weps:
        return
       
    size_x, size_y = im.size
    ts = BARREL_LENGTH # tank gun offset
    tsy = BARREL_LENGTH_Y
    sf = sfv.get()#SF
    wf = wfv.get()#WF
    apogee_cnt_init = 0
    max_t = MAX_T
    gacc = gaccv.get()#GACC
    gacc_sub = gacc*10.0/9.803#GACC_SUB
    tasermax = 1
    taserangle = [angle, angle]
    bouncing = False
    bf = 0.0 # boomerang
    ram = False
    ram_active = False
   
    if weapon in slow_weps or md.get() == 2:
        #md.set(2)
        if PRINT:
            print("SLOW")
        sf = sf/2.0
        wf = wf/2.0#WF_SLOW
        gacc = gacc/4.0
        gacc_sub = gacc_sub/4.0
    elif weapon in hover_wep or md.get() == 3:
        #md.set(3)
        if PRINT:
            print("HOVER")
        apogee_cnt_init = int(HOVER_T)
        if weapon == "Heavy Hover-Ball":
            apogee_cnt_init = apogee_cnt_init*10//8
        # Heavy flies 1.0s, normal 0.8s
    elif weapon in straight_weps or md.get() == 4:
        #md.set(4)
        if PRINT:
            print("STRAIGHT")
        gacc = 0.0
        gacc_sub = 0.0
        wind = 0.0
    elif weapon in drei_d_wep or md.get() == 5: # 3d bomb is different with wind, not quite right yet (shot up/dn is deviating more in real game)
        #md.set(5)
        if PRINT:
            print("3D")
        gacc = 0.0
        gacc_sub = 0.0
        # wf = 0.00005
        max_t = BOMB_T # limit flight time
    elif weapon in taser_wep or md.get() == 6:
        #md.set(6)
        if PRINT:
            print("TASER")
        tasermax = 2
        taserangle = [angle-5.7, angle+5.7]
    elif weapon in bounce_wep or md.get() == 7:
        #md.set(7)
        if PRINT:
            print("BOUNCING")
        if override.get() == 0:
            bfactor.set(BOUNCE_FACTOR[0])
        bouncing = True
        max_t = 2*max_t
    elif weapon in medium_bounce_wep or md.get() == 11:
        #md.set(11)
        if PRINT:
            print("MEDIUM-BOUNCING")
        if override.get() == 0:
            bfactor.set(BOUNCE_FACTOR[1])
        bouncing = True
        max_t = int(1.5*max_t)
    elif weapon in highbounce_wep or md.get() == 8:
        #md.set(8)
        if PRINT:
            print("HIGH-BOUNCING")
        if override.get() == 0:
            bfactor.set(BOUNCE_FACTOR[2])
        bouncing = True
        max_t = 2*max_t
    elif weapon in ultra_bounce_wep or md.get() == 12:
        #md.set(12)
        if PRINT:
            print("ULTRAHIGH-BOUNCING")
        if override.get() == 0:
            bfactor.set(BOUNCE_FACTOR[3])
        bouncing = True
        max_t = 2*max_t
    elif weapon in boomerang_wep or md.get() == 9:
        #md.set(9)
        sx = power*math.cos(angle*math.pi/180.0)
        bf = wf*BOOMERANG_F*sx/(100.0*math.cos(45.0*math.pi/180.0))
        if PRINT:
            print("BOOMERANG",sx,bf)
    elif weapon in ram_wep or md.get() == 10:
        #md.set(10)
        ram = True # Trigger grav increase on apex
        gacc_ram = gacc*RAM_F
        if PRINT:
            print("RAM")
    else:
        pass#md.set(0)
    sw = subwep.get() != 0
    if weapon in submersive_weps:
        sw = True
    pixdata = im.load()
    pixdata[x, y] = (255, 0, 0)
    pixdata[x+1, y+1] = (255, 0, 0)
    pixdata[x+1, y] = (255, 0, 0)
    pixdata[x, y+1] = (255, 0, 0)
    #bcnt=0 # dbg
    for taserp in range(0, tasermax):
        if tasermax == 2:
            angle = taserangle[taserp]
        for q in range(0, 2): # ugly workaround for aiming dir (not unique)
            sx = sf*power*math.cos(angle*math.pi/180.0)
            sy = sf*power*math.sin(angle*math.pi/180.0)
            lx = x+ts*math.cos(angle*math.pi/180.0)
            if q==1:
                lx = x-ts*math.cos(angle*math.pi/180.0)
            ly = y-tsy*math.sin(angle*math.pi/180.0)
            p = [[lx, ly], [lx, ly]]
            renderbuf = [(0, 0)]*max_t
            rbcnt = 0
            windacc = 0.0
            rfcooldown = 0
            bouncing_cnt = 0
            hover = False
            apogee_cnt = apogee_cnt_init
            for t in range(0, max_t):
                oldsy = sy
                if q==0:
                    p[q][0] += sx
                else:
                    p[q][0] -= sx
                p[q][0] += windacc
                if q==0:
                    bff=-bf
                windacc += wind*wf + bff
                if not hover:
                    p[q][1] -= sy
                ix = int(p[q][0]+0.5)
                iy = int(p[q][1]+0.5)
                # if q==0:
                    # print(ix, iy, gnd[ix], sy)
                if bouncing: # hitting the ground bounces the weapon
                    if ix<0 or ix>=size_x-1 or iy<=gnd[ix] or bouncing_cnt>0 or sy>0: # shot in field and in air, upwards, or bouncing cooldown
                        if bouncing_cnt>0:
                            bouncing_cnt-=1
                    else:
                        # print("bouncing")
                        bouncing_cnt=5
                        p[q][1] = gnd[ix]+1 # ugly hack to prevent endless bouncing
                        gnd_angle,dx,dy = calc_gnd_angle(gnd, ix)
                        # calc entry/exit angles and modify sx,sy
                        sxn, syn = calc_refl_angle(gnd_angle, sx, sy)
                        #if q==0 and bcnt==0:
                        #    bcnt+=1
                        #    print("angle:", gnd_angle*180/math.pi,dx,dy)
                        #    print("in:",sx,sy)
                        #    print("out:",sxn,syn)
                        sy = syn * bfactor.get()#BOUNCE_FACTOR[btype]
                        sx = sxn * bfactor.get()#BOUNCE_FACTOR[btype]
                    sy -= gacc # normal flight curve
                elif sw:
                    if ix<0 or ix>=size_x-1 or iy<=gnd[ix]: # shot in field and in air
                        if not hover:
                            sy -= gacc
                    else:
                        sy += gacc_sub # underground
                        # print("sub")
                else:
                    if not hover:
                        sy -= gacc
                if oldsy>0 and sy<0 and apogee_cnt>0:
                    hover = True
                    # print("Apogee detected", t)
                if ram and oldsy>0 and sy<0:
                    ram_active = True
                if ram_active:
                    gacc = gacc_ram
                if hover:
                    apogee_cnt -= 1
                    if apogee_cnt == 0:
                        hover = False
                if bound.get()!=0 and ix >= size_x and rfcooldown==0:# and iy in range(gnd[-1]-BOUNDER_HEIGHT,gnd[-1]+BOUNDER_HEIGHT): # right bounder
                    sx = -sx
                    rfcooldown = 10
                    windacc = -windacc
                if bound.get()!=0 and ix <= 0 and rfcooldown==0:# and iy in range(gnd[0]-BOUNDER_HEIGHT,gnd[0]+BOUNDER_HEIGHT): # left bounder
                    sx = -sx
                    rfcooldown = 10
                    windacc = -windacc
                if rfcooldown>0:
                    rfcooldown -= 1
                if ix in range(0, size_x) and iy in range(0, size_y):
                    renderbuf[rbcnt] = (ix, iy, COLOR[q])
                    rbcnt += 1
                if ix not in range(-10, size_x+10): # end meaningless calculation
                    break
                if iy > size_y+10 and sy < 0: # end meaningless calculation
                    break
            for t in range(0, rbcnt):
                ix, iy, c = renderbuf[t]
                pixdata[ix, iy] = c # draw curve
                try:
                    pixdata[ix, iy+1] = c # draw curve
                    pixdata[ix+1, iy] = c # draw curve
                    pixdata[ix+1, iy+1] = c # draw curve
                except:
                    pass
    if verbose:
        im.save("sss.jpg")
    im_r = im.resize((int(size_x/IMG_SCALE), int(size_y/IMG_SCALE)), 3)
    img = ImageTk.PhotoImage(im_r)
    canvas.create_image(0,0,anchor=NW,image=img)
def find_tank(im):
    im_tank = im.load()
    found = False
    x = 0
    y = 0
    size_x, size_y = im.size
    for y in range(size_y-1, 0, -2):
        for x in range(0, size_x, 2):
            r, g, b = im_tank[x, y]
            if (r in range(100, 140)) and (g in range(200, 255)) and (b in range(100, 140) and (g-b > 90)): # color matching the green tank
                tank_at = (x, y)
                found = True
                break
        if found:
            break
    if found==False:
        return (0, 0)
    p = [[0]*2]*40*40
    cnt = 0
    for xx in range(x-40, x+40):
        for yy in range(y-40, y+40):
            if xx>=0 and yy>=0 and xx<size_x and yy<size_y:
                r, g, b = im_tank[xx, yy]
                if (r in range(0, 127)) and (g in range(160, 255)) and (b in range(0, 127) and (g-b > 100)):
                    try:
                        p[cnt] = [xx, yy]
                        # im_tank[xx,yy] = (255,0,0) # highlight tank pixels
                        cnt += 1
                    except:
                        pass
    if cnt > 0:
        xsum = 0
        ysum = 0
        for pp in p:
            xsum += pp[0]
            ysum += pp[1]
        xsum /= cnt
        ysum /= cnt
        rx, ry = int(xsum+0.5), int(ysum+0.5) + TANKY_OFFS
        # im_tank[rx,ry] = (0,0,0) # use this to precision-match the center
        # im_tank[rx+1,ry] = (0,0,0)
        # im_tank[rx,ry+1] = (0,0,0)
        # im_tank[rx+1,ry+1] = (0,0,0)
        # im.show()
        return (rx, ry)
    return (0, 0)
def find_gnd_color(im, size_x, size_y): # find the general color of the ground, possible improvements: sample more, only take majority color
    # try left bottom
    col = im[10, size_y-10]
    if col[0] in range(50,255) or col[1] in range(50,255) or col[2] in range(50,255): # with the risk that a tank or obstacle is there
        return col
    col = im[size_x//2, size_y-10]
    if col[0] in range(50,255) or col[1] in range(50,255) or col[2] in range(50,255): # with the risk that a tank or obstacle is there
        return col
    col = im[size_x-10, size_y-10]
    if col[0] in range(50,255) or col[1] in range(50,255) or col[2] in range(50,255): # with the risk that a tank or obstacle is there
        return col
def check_col(p, col, dn=-20, dp=50):
    if p[0] in range(col[0]-dn,col[0]+dp) and p[1] in range(col[1]-dn,col[1]+dp) and p[2] in range(col[2]-dn,col[2]+dp):
        return True
    return False
def find_y_border(im, col, x, size_x, size_y):
    y = size_y-1 # scan from bottom -> up
    en = False
    collimit = 120
    enlimit = 200
    if x >= size_x-25 or x <= 25: # darkened border reagon
        collimit = 70
        enlimit = 100
    while y>0:
        p = im[x, y]
        if p[2] > enlimit:
            en = True
        if en and p[2] < collimit:# massive decrease in blue brightness -- check_col(p, col, -20, 50): # already gnd color
            return y+1
        y -= 1
        #xxx find tanks
    return size_y-1
def find_ground(im):
    im_gnd = im.load()
    size_x, size_y = im.size
    ground = [-1]*size_x
    gnd_col = find_gnd_color(im_gnd, size_x, size_y)
    if PRINT:
        print("Ground color: ", gnd_col)
    for x in range(0,size_x): # todo multithreaded loop to speed up
        y = find_y_border(im_gnd, gnd_col, x, size_x, size_y)
        # print(x,y)
        # im_gnd[x, min(y,size_y-1)] = (255,0,0)
        # im_gnd[x, min(y+1,size_y-1)] = (255,0,0)
        # im_gnd[x, min(y+2,size_y-1)] = (255,0,0)
        ground[x] = y
    # im.show()
    # interpolate missing gnd?
    return ground
im_play = ""
running = False
def run_click():
    global running
    # run()
    # return
    if running:
        btn_run["text"] = "Run"
        running = False
    else:
        btn_run["text"] = "Stop"
        running = True
        run()
img_wep = ""
def run():
    global im_play, img_wep
    # global wind, weapon, power, angle
    im = pyautogui.screenshot()
    im = im.crop(GAME_CRD)
    if AUTO:
        f_rect = FIRE_CRD
        im_f = im.crop(f_rect)
        im_fire = im_f.load()
        c = im_fire[0, 0]
        r, g, b = c
        # print((r,g,b))
        if not (r>220 and g<40 and b<40):#r==g and g==b
            if running:# and not stop_event.is_set():
                win.after(10, run)
            return
    if override.get()==0:#wind == "":
        wind_rect = WIND_CRD#(946, 102, 943+29, 102+18)
        wind.set(f"{get_wind(im, wind_rect)}")
    weapon = "not scanned"
    if autowep.get() != 0:
        wep_rect = WEP_CRD
        im_wep = im.crop(wep_rect)
        img_wep = ImageTk.PhotoImage(im_wep)
        for o in OCRS:
            weapon = ocr(im_wep, o, '--psm 7').strip()
            if weapon in WEAPONS:
                break
        if SHOW_CROPS:
            im_wep.show()
        lw["text"] = weapon
    play_rect = PLAY_CRD#(264, 34, 264+1393, 71+854)
    im_play = im.crop(play_rect)
    #start = timer()
    # find green tank's x and y
    x, y = find_tank(im_play)
    if y==0:
        if PRINT:
            print("Tank not found!")
    if PRINT:
        print("Tank at: ", x, y)
    gnd = find_ground(im_play)
    #print("find stuff:",timer()-start)
    canvas.delete("all")
    if y != 0:
        # pow_rect = ((max(0, x-42), y+22, min(x+42, size_x), min(y+54+17, size_y)), (max(0, x-105), y-10, max(1, x-20), min(y+20, size_y))) # match the 2 possible positions
        # for pr in pow_rect:
        pr = find_pa(im_play, x, y)
        # print(pr,x,y)
        if pr[1] < pr[3]:
            col = "#888"
            fnt = "Tahoma 14"
            ocrm = OCRS#["paddleocr", "winrt", "tesseract"]
            for m in ocrm: # sorted by accuracy
                try:
                    pa = get_pow_ang_t(im_play, pr, m).strip()
                    pap = postprocess(pa)
                except:
                    pap = ""
                if pap != "" and not BENCH_OCR: # sim shot
                    p, a = pap
                    if p.strip() != "" and a.strip() != "":
#                        try:
                            if override.get() == 0: #power == "":
                                power.set(f"{int(p)}")
                            if override.get() == 0: #angle == "":
                                angle.set(f"{int(a)}")
                            sim_shot(im_play, x, y, int(power.get()), int(angle.get()), int(wind.get()), weapon, gnd)
                            if img_pa != "":
                                canvas.create_image(5, 5, anchor=NW, image=img_pa)
                            canvas.create_text(180, 20, text=f"{pa}", fill=col, font=fnt)
                            break
 #                       except:
                            if PRINT:
                                print("invalid pa")
                    else:
                        if PRINT:
                            print("invalid pa")
            if BENCH_OCR: # show ocr activity
                if img_wind != "":
                    canvas.create_image(10, 5, anchor=NW, image=img_wind)
                    canvas.create_text(150, 14, text=f"{wind.get()}", fill=col, font=fnt)
               
                ox = 350
                for o in OCRS:
                    start = timer()
                    pa = get_pow_ang_t(im_play, pr, o).strip()
                    t = "{:1.1f}".format((timer()-start)*1000.0)
                    canvas.create_text(ox, 80, text=o+f":\n{pa}\n{t}", fill=col, font=fnt)
                    ox += 145
                if img_pa != "":
                    canvas.create_image(10, 80-40, anchor=NW, image=img_pa)
                if img_wep != "":
                    canvas.create_image(250, 0, anchor=NW, image=img_wep)
    if AUTO:
        if running:# and not stop_event.is_set():
            win.after(10, run)
x=""
stop_event = threading.Event()
def run_auto():
    while(True):
        run()
        time.sleep(0.05)
        if stop_event.is_set():
            break
def save():
    im_play.save("sss.jpg")
#Create a canvas
size_x = PLAY_CRD[2]-PLAY_CRD[0]
size_y = PLAY_CRD[3]-PLAY_CRD[1]
canvas = Canvas(win, width=int(size_x/IMG_SCALE), height=int(size_y/IMG_SCALE))
canvas.pack()
btn_run = Button(win, text = "Run", width = 200, command = run_click)
btn_run.pack()
f1 = Frame(win)
Checkbutton(f1, text="Enable Underground", variable=subwep).pack(side='left')
Checkbutton(f1, text="Enable Bounders", variable=bound).pack(side='left')
Checkbutton(f1, text="Detect Weapon", variable=autowep).pack(side='left')
Label(f1, text="SF:").pack(side=LEFT)
Entry(f1, textvariable=sfv, width=10).pack(side=LEFT)
Label(f1, text="WF:").pack(side=LEFT)
Entry(f1, textvariable=wfv, width=10).pack(side=LEFT)
Label(f1, text="G:").pack(side=LEFT)
Entry(f1, textvariable=gaccv, width=10).pack(side=LEFT)
f1.pack(fill='x', expand=True)
f2 = Frame(win)
lw = Label(f2, text="Weapon:")
lw.pack(side=LEFT)
Label(f2, text="Wind:").pack(side=LEFT)
Entry(f2, textvariable=wind, width=4).pack(side=LEFT)
Label(f2, text="Power:").pack(side=LEFT)
Entry(f2, textvariable=power, width=4).pack(side=LEFT)
Label(f2, text="Angle:").pack(side=LEFT)
Entry(f2, textvariable=angle, width=4).pack(side=LEFT)
Label(f2, text="Bounce:").pack(side=LEFT)
Entry(f2, textvariable=bfactor, width=4).pack(side=LEFT)
Checkbutton(f2, text="Override", variable=override).pack(side=LEFT)
Button(f2, text = "Save", command = save).pack(side=LEFT)
f2.pack(fill='x', expand=True)
f3 = Frame(win)
Radiobutton(f3, text="Normal", variable=md, value=0).pack(side=LEFT)
Radiobutton(f3, text="Slow", variable=md, value=2).pack(side=LEFT)
Radiobutton(f3, text="Hover", variable=md, value=3).pack(side=LEFT)
Radiobutton(f3, text="3D", variable=md, value=5).pack(side=LEFT)
Radiobutton(f3, text="Taser", variable=md, value=6).pack(side=LEFT)
Radiobutton(f3, text="Bounce", variable=md, value=7).pack(side=LEFT)
Radiobutton(f3, text="Med-", variable=md, value=11).pack(side=LEFT)
Radiobutton(f3, text="High-", variable=md, value=8).pack(side=LEFT)
Radiobutton(f3, text="Ultra-Bounce", variable=md, value=12).pack(side=LEFT)
Radiobutton(f3, text="Boomerang", variable=md, value=9).pack(side=LEFT)
Radiobutton(f3, text="Ram", variable=md, value=10).pack(side=LEFT)
f3.pack(fill='x', expand=True)
def on_close():
    global x, stop_event
    if AUTO:
        stop_event.set()
        #x.join()
    with open(CONFIG_FILE, "w") as conf:
        conf.write(win.geometry())
    win.destroy()
win.protocol("WM_DELETE_WINDOW", on_close)
def key_handler(event):
    if event.keycode == 27:
        sys.exit()
    if event.char == "r" or event.keysym == "space":
        run_click()
    if event.char == "s":
        save()
    if event.char == "o":
        override.set(1-override.get())
    if event.char == "b":
        bound.set(1-bound.get())
    if event.char == "u":
        subwep.set(1-subwep.get())
    # print(event.char, event.keysym, event.keycode)
win.bind("<Key>", key_handler)
#if AUTO:
#    x = threading.Thread(target=run_auto)
#    x.start()
# loop
win.mainloop()
stop_event.set()
#~ # the math solution
#~ # calc shellshock
#~ C = 573.0/(0.73 * 2.58 * math.cos(math.pi/4.0))
#~ G = (0.73 * math.sqrt(2.0) * C)/2.58
#~ D = 446.0 # Durchmesser Zielkreis
#~ B = 446.0/D/1.53 # screen correction
#S = 2.82/1.51
#~ def cr(x, y, angle):
    #~ x = B*x
    #~ y = B*y
    #~ ang = angle*math.pi/180.0
    #~ num = G*x*x
    #~ den = C*C*(x*math.sin(2.0*ang) - 2.0*y*(math.cos(ang) ** 2.0))
    #~ k = math.sqrt(num/den)
    #~ return k * 100.0
#~ print(cr(385, -226, 30))

DIY Sub-35g Digital Whoop

I built a small 1s digital whoop with WalkSnail.

Vision 40

Based on a Mobula 7 frame, T-Motor F411 AIO 13A 1s, RcINpower 1002 motors with Gemfan 1610 props, BetaFPV ELRS lite and the Walksnail VTX lite.

It weighs 34g, a 1s Lipo 550mAh has 14g. Flight-time with Tattu 1s 500mAh is 3:15. The GNB claiming 550mAh are only at 2:45.

UART1 = ELSR, UART2 = WalkSnail (CRSF), the motors are directly soldered. I used the original BLHeli_32 suite to fix the motor directions. The canopy is from https://www.printables.com/de/model/504099-rotorriot-vision40-prints.

OSD activation is a bit tricky:

set osd_displayport_device = MSP
set displayport_msp_serial = 1

Complete Diff:

# diff all
# version
# Betaflight / STM32F411 (S411) 4.3.1 Jul 13 2022 / 03:32:52
(8d4f005) MSP API: 1.44
# config: manufacturer_id: TMTR, board_name: TMOTORF411,
version: edcd244b, date: 2022-06-29T02:24:46Z
 
# start the command batch
batch start
 
# reset configuration to default settings
defaults nosave
 
board_name TMOTORF411
manufacturer_id TMTR
mcu_id 003700413236510435383430
signature
 
# name: V40
 
# feature
feature -RX_PARALLEL_PWM
feature -LED_STRIP
feature TELEMETRY
 
# serial
serial 0 64 115200 57600 0 115200
serial 1 1 115200 57600 0 115200
 
# beeper
beeper -ON_USB
 
# beacon
beacon RX_LOST
beacon RX_SET
 
# map
map TAER1234
 
# aux
aux 0 0 0 1750 2100 0 0
aux 1 1 1 1700 2100 0 0
aux 2 13 2 1700 2100 0 0
aux 3 35 2 1750 2100 0 0
 
# master
set gyro_lpf1_static_hz = 0
set gyro_lpf2_static_hz = 0
set dyn_notch_count = 2
set dyn_notch_min_hz = 100
set dyn_notch_max_hz = 1000
set gyro_lpf1_dyn_min_hz = 0
set gyro_lpf1_dyn_max_hz = 750
set acc_calibration = 55,52,-4,1
set mag_hardware = NONE
set baro_hardware = NONE
set rc_smoothing_auto_factor = 80
set rc_smoothing_setpoint_cutoff = 25
set rc_smoothing_feedforward_cutoff = 25
set serialrx_provider = CRSF
set rx_spi_protocol = EXPRESSLRS
set dshot_bidir = ON
set motor_pwm_protocol = DSHOT300
set motor_poles = 12
set align_board_pitch = -4
set vbat_max_cell_voltage = 440
set ibata_scale = 400
set ibata_offset = -1700
set beeper_dshot_beacon_tone = 3
set yaw_motors_reversed = ON
set small_angle = 180
set pid_process_denom = 1
set simplified_gyro_filter_multiplier = 150
set osd_cap_alarm = 450
set osd_vbat_pos = 2432
set osd_link_quality_pos = 2048
set osd_tim_2_pos = 2080
set osd_throttle_pos = 2368
set osd_current_pos = 2464
set osd_mah_drawn_pos = 2528
set osd_display_name_pos = 0
set osd_warnings_pos = 14355
set osd_displayport_device = MSP
set debug_mode = GYRO_SCALED
set displayport_msp_serial = 1
set rpm_filter_harmonics = 2
set name = V40
 
profile 0
 
# profile 0
set vbat_sag_compensation = 100
set anti_gravity_gain = 10000
set iterm_relax_cutoff = 10
set p_pitch = 67
set i_pitch = 120
set d_pitch = 76
set f_pitch = 199
set p_roll = 64
set i_roll = 115
set d_roll = 67
set f_roll = 191
set p_yaw = 64
set i_yaw = 115
set f_yaw = 191
set d_min_roll = 67
set d_min_pitch = 76
set thrust_linear = 40
set feedforward_averaging = 2_POINT
set feedforward_smooth_factor = 45
set feedforward_jitter_factor = 10
set dyn_idle_min_rpm = 35
set simplified_master_multiplier = 160
set simplified_d_gain = 140
set simplified_pi_gain = 90
set simplified_dmax_gain = 0
 
profile 1
 
# profile 1
set dterm_lpf1_dyn_min_hz = 100
set dterm_lpf1_dyn_max_hz = 125
set dterm_lpf1_type = BIQUAD
set dterm_lpf2_static_hz = 0
set vbat_sag_compensation = 100
set anti_gravity_gain = 4500
set crash_recovery = ON
set iterm_relax_cutoff = 12
set p_pitch = 58
set i_pitch = 83
set d_pitch = 70
set f_pitch = 124
set p_roll = 56
set i_roll = 79
set d_roll = 61
set f_roll = 119
set p_yaw = 56
set i_yaw = 79
set f_yaw = 119
set d_min_roll = 52
set d_min_pitch = 59
set thrust_linear = 40
set dyn_idle_min_rpm = 60
set simplified_master_multiplier = 125
set simplified_i_gain = 80
set simplified_d_gain = 140
set simplified_dmax_gain = 50
set simplified_feedforward_gain = 80
set simplified_dterm_filter = OFF
 
profile 2
 
# restore original profile selection
profile 1
 
rateprofile 0
 
# rateprofile 0
set thr_mid = 42
set thr_expo = 30
set rates_type = BETAFLIGHT
set roll_rc_rate = 100
set pitch_rc_rate = 100
set yaw_rc_rate = 100
set roll_expo = 40
set pitch_expo = 40
set roll_srate = 80
set pitch_srate = 80
set yaw_srate = 80
 
rateprofile 1
 
# rateprofile 1
set rates_type = BETAFLIGHT
set roll_rc_rate = 80
set pitch_rc_rate = 80
set yaw_rc_rate = 80
set roll_expo = 40
set pitch_expo = 40
set roll_srate = 70
set pitch_srate = 70
set yaw_srate = 70
 
rateprofile 2
 
rateprofile 3
 
rateprofile 4
 
rateprofile 5
 
# restore original rateprofile selection
rateprofile 0
 
# save configuration
save

Copter-Tuning revisited

Es gibt einen neuen Tuning-Guide von Chris Rosser:

Betaflight 4.4 Tuning von Chris Rosser

Vorbereitung

  1. Flugfähiger Copter mit BF 4.3 oder 4.4
  2. RC-Link-Preset angewandt
  3. Blackbox-Logging auf 1/2 PID-Loop-Speed mit GYRO_SCALED
  4. Bi-Directional DSHOT, d.h. BLHeli32 oder Bluejay (die Anzahl Pole richtig setzen: 12 für kleine Motoren, 14 für normale)
  5. Optional OSD, zum PID-Tunen ohne PC

Filter-Tuning

Chris schaltet alle Gyro-Lowpass-Filter ab, außer den dynamischen und die RPM-Filter. Nur einen D-Term-Lowpass und den dafür auf BIQUAD. Die Grenzfrequenzen hängen von der Coptergröße ab, ein 40mm bis 2″ sollte so 100 und 125Hz haben. Die Filter kann man im Blackbox-Explorer überprüfen.

Der Testflug sollte einige 100%-Throttle, Vollgas-Punchouts haben.

PD-Tuning

Vor dem Tuning stellt Chris den I-Regler auf 0.

Die Testflüge macht er im Angle-Mode, da in diesem Mode der Feedforward inaktiv ist.

P-D-Balance und Master-Regler

Der Copter wird einfach im Schwebeflug gehalten und dabei schnell hin und her gewackelt und vor/zurück. Chris macht eine Serie an Reglerpositionen auf einen Akku und bewegt den Regler im OSD. Zuerst wird die Balance eingestellt. Chris fliegt ein paar PD-Balance-Werte, z.B. 0.3, 0.4, 0.5, 0.75, 1.0, 1.25 und 1.5.

Nach der Testflugreihe zieht er das Log auf den PC und analysiert es mit der PID-Toolbox mit der Step-Response (Sprungantwort). Ziel ist es, schnell auf 1.0 zu kommen ohne über zu schwingen.

Sobald die optimale Balance gefunden wurde, wird der Master-Gain eingestellt. Z.B. die Werte 1.0, 1.25, 1.5, 1.75 und 2.0. Sollte der Weg des Reglers nicht reichen (gerade bei kleinen Coptern), gibt es einen Trick: Man stellt die individuellen Regler für P und D auf jeweils das Doppelte und den Master wieder auf die Mitte.

Man kann diese zwei Schritte auch wiederholen, um die Balance bei hohem Mastergain noch besser einzuregeln.

Feedforward einstellen

Ich habe mich mit FF noch nicht so anfreunden können, das liegt evtl. daran, dass meine Quads nicht viel darauf reagieren. Ziel ist es, den Delay zwischen Stick-Input („Setpoint“) und Gyro-Sensor auf 0 zu bekommen, der Quad „erahnt“ quasi die Knüppelbewegungen (reagiert auf die Knüppelbeschleunigung). Das klappt nicht immer 100%ig.

Chris stellt den I-Regler wieder auf 1.0. Man kann mit ein paar Probeflügen noch den I-Anteil feintunen, wie gut der Quad bei Punchouts die Lage hält und in der PID-Toolbox die Achsen an der 1.0 kleben ohne träger zu werden. Das Tuningfenster ist aber recht groß.

Chris testet die Reglerwerte 1.0, 1.25 und 1.5. Sollte sich die Verzögerung nicht verbessern oder die Kurven kräftig gestört sein, sollte man wieder eine Stufe zurück gehen.

Extras

Chris empfiehlt unbedingt „Dynamic-Idle“ zu aktivieren. Der Wert hängt von der Leerlaufdrehzahl ab. Für kleine 40mm stellt er 94 ein, 2″ 75, 3″ 50, etc. Dyn.Idle RPM = ~15000/Propeller-Durchmesser in Zoll. Der Wert in BF ist dann diese RPM geteilt durch 100.

Dann stellt man noch die Rates und die RC-Filterung (RC-Smoothing) noch nach eigenen Vorlieben ein.

Ich empfehle auch noch „crash_recovery=on“.

Und Arming-Angle 180°.

Sonstige Hinweise

Das Tuning hängt natürlich auch vom Akku ab, sollte man 2s,3s,4s,etc wild mischen, kann man die Presets nutzen und die automatische Umschaltung abhängig von der Zellenzahl „auto_profile_cell_count“. Auch Änderungen im Gewicht beeinflussen das Tuning (Gopro drauf oder nicht, großer Longrange-Lipo vs kleiner Lipo, etc)!

Mobula 7 2s update

Ja, ich hab ihn noch.

Auf BF 4.2.9, mit neuen Einstellungen:

Crash_recovery=ON

Dynamic RPM-Filter und Bluejay 0.2.0 48kHz (ich hab das Crazybee F4 V3 Pro mit BLHeli_S F_H_40)

So LOS-Schweben klingt er unglaublich smooth und stabil. Bei leichten absichtlich ausgeführten Crashs in z.B. den Wäscheständer stabilisiert er sich sofort. Toll!

Als nächstes werden die Lipo-Stecker auf BT2.0 oder GNB27 ausgetauscht. Die Motorstecker bleiben, die kleinen 0802 sind doch recht schnell durchgenudelt…

Licht-Fernsteuerung mit ESP32

Empfänger

Steuerung von bis zu 4 Slaves, mit je 10 LEDs. Das ist natürlich erweiterbar.

Die Slaves unterstützen WS2812 LEDs (an nodemcu:io15 oder esp32:io16) oder direkte Portpins (ESP32).

const byte lpin[NUMPIXELS] = {
  27, // Licht 1
  25,
  32,
  12,
  4,
  22,
  21,
  17,
  23,
  19  // Licht 10
};

Der Master hat einen Slave-Select-Input und 10 LED-Toggle-Tasten.

Die Funkverbindung läuft über ESP_NOW.

Langer Druck (ca. 3s) auf Select startet einen neuen Scan nach Slaves.

Master-Button-Pins:

const byte btnpins[NUMBUTTONS] = {
  22, // slave select (long press for rescan slaves)
  27, // licht 1-10 (long press for fw update on slave id 0-9)
  14,
  12,
  23,
  32,
  33,
  25,
  26,
  19,
  18
};

Die Geräte haben alle Wifi-Webseiten. Sie heißen in der WLAN-Suche „LICHTSENDER“ bzw. „SlaveX:MAC-Adresse“ (X zählt hoch, ab 0). Das WIFI-Passwort ist „esp32licht“. Die IP-Adresse des Servers http://10.0.0.1.

Die Firmware lässt sich über Wifi updaten. Die Slaves kann man auch konfigurieren (ID, Lichtmodi, etc.)

Das hat mir mit dem Rückkanal sehr geholfen: https://www.esp32.com/viewtopic.php?t=13522

Onkyo K-611 Fehlersuche und Workaround

Mein niedliches 3-Kopf-Tapedeck K-611 spielt nicht. Bzw. sehr unregelmäßig geht es mal. Bevorzugt nach langer Standzeit.

Das Symptom: Der Kopf fährt 2 mal (oder öfters) auf die Play-Position hoch und reversiert gleich wieder.

Nachdem ich bestimmt 5-6 mal das Ding zerlegt hatte, Riemen getauscht, geputzt, neu geschmiert, was weiß ich – kam ich auf die grandiose Idee, mal die Mikroschalter anzuschauen:

Das sind die zwei Positionsschalter für den Kopfschlitten. Die 5V liegen nur während der Fahrt an.

Unten (gelb) sieht man den Schalter für „unten“ und oben (blau) für Play. Da man 2 Versuche sieht (und dazwischen die 5V ausgehen), wird klar, dass der blaue nie aktiviert.

Jetzt sind diese Schalter blöderweise ganz innen in der Mechanik hinter den Riemen und den Motoren vergraben…

Also: Ich simuliere den oberen Schalter einfach manuell!

Testweise den Schalter manuell brücken.
Der Workaround – man drückt den Schalter zur rechten Zeit!

Lösung: Einfach einen externen Schalter zum manuell betätigen anbringen. Ja, dirty hack, ich weiß… es klappt aber!

Das Innenleben:

Das Gerät ist erstaunlich komplex. 4 Motoren arbeiten da drin (Capstan, Spulen, Schlitten und Tür)! 3 Riemen (Capstan, Tür und Schlitten), den Schlittenriemen habe ich natürlich nach den Symptomen als erstes ausgetauscht. Den Türriemen habe ich zumindest „aufgefrischt“ (Kochen in Wasser).

Reparieren ist enorm aufwändig – viele Steckverbinder und Schrauben gilt es zu lösen und wieder zu montieren. Ich habe sogar mal die Capstan neu geschmiert.

Soweit spielt das Deck wieder gut. Operation geglückt!

Neues

ExpressLRS gibt’s 3.0

  • mit WIFI-Betaflight-Config-Link.

DJI FPV Goggles gehackt

  • fpvwtf hat das BF-OSD auf die DJI-Hardware gebracht.

Ich hab eine neue Fritzbox

  • VPN geht wieder.
  • Fritz!fon-DECT-Telefon ist bestellt.
  • Die Box ist viel schneller als der Schrott von Vodafone!

Sharp Pocket Computer PC-1360

Revival nach 17(!) Jahren.

Mehr Infos siehe www.lehmayr.de

Zum Verbinden über UART mit den „modernen“ FTDI-Arduino-Adaptern:

Am Pocket Pin 5+8 auf 5V (Pin 10 und 13).

Pin2 = TX, Pin3 = RX. Pin11 = Ready to receive.

Im FT_PROG die RX/TX invertieren!

Der Pin11 geht über einen Transistorinverter auf den UART CTS!

Danach kann man ganz normal mit LOAD und SAVE und INPUT #1Varname(*) Daten austauschen.

Den 2kB RAM kann man mit dem Hilfprogramm in L17 befüllen. Am Pocket GOTO 17, am PC die Datei 2kB-RAM.txt senden.

Die Variablen habe ich einzeln mit DIM angelegt. Beispiel: DIM M$(0,13)*72. Dann kann man sie mit INPUT #1M$(*) übertragen.

Wichtig: Die innere Uhr hängt auch am 15pol. Anschluss, die geht nur, wenn die Verbindung abgesteckt wurde!

Stellen der Uhr mit GOTO 8200 und GOTO 8205.

Spiele sind in DEF S.

Später habe ich den alten COMPAQ-PC aufgestellt und TransDrive/Diskettenemulator verbunden. Dann den alten PC mit dem Laptop per Nullmodem-Kabel verbunden. Auf dem Laptop lief Dosbox unter Linux mit einem USB-serial-Adapter. Der alte Norton Commander NC5 übertrug dann die Dateien! Zwischen den Computern liegen über 30 Jahre!

Fazit:

Der Weg über die serielle Schnittstelle, dem FTDI-USB-Arduino-Adapter und meinem PocketASM-Tool ist deutlich bequemer.

Mit diesem BASIC-Loader kann man auch (fast) alles laden:

10 CLOSE : OPEN : INPUT X: FOR X=X TO 65535: INPUT #1Y: POKE X,Y: NEXT X

Am PC muss man halt die Daten in eine Textliste umformatieren, jede Zeile eine Zahl.

Wenn man die ganze Karte sichern will, geht das am bequemsten so:

20 CLOSE : OPEN : INPUT X: FOR X=X TO 65535: PRINT #1 PEEK X: NEXT X

Unter Linux geht das mit cutecom:

Wichtig: Damit der FTDI-UART läuft, muss man brltty (Blindenterminal-Daemon) entfernen!