From b399ff63ce11c38e09618aab518594780a4e0858 Mon Sep 17 00:00:00 2001 From: sakumisu <1203593632@qq.com> Date: Tue, 23 Jul 2024 22:37:27 +0800 Subject: [PATCH] feat(demo): add uf2 demo --- README.md | 1 + README_zh.md | 1 + demo/bootuf2/bootuf2.c | 430 ++++++++++++++++++++++++++++ demo/bootuf2/bootuf2.h | 228 +++++++++++++++ demo/bootuf2/bootuf2_config.h | 25 ++ demo/bootuf2/cherryuf2.png | Bin 0 -> 19847 bytes demo/bootuf2/msc_bootuf2_template.c | 163 +++++++++++ demo/msc_ram_template.c | 2 +- tools/uf2/uf2conv.py | 361 +++++++++++++++++++++++ 9 files changed, 1210 insertions(+), 1 deletion(-) create mode 100644 demo/bootuf2/bootuf2.c create mode 100644 demo/bootuf2/bootuf2.h create mode 100644 demo/bootuf2/bootuf2_config.h create mode 100644 demo/bootuf2/cherryuf2.png create mode 100644 demo/bootuf2/msc_bootuf2_template.c create mode 100644 tools/uf2/uf2conv.py diff --git a/README.md b/README.md index 5644f60..011d222 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ CherryUSB Device Stack has the following functions: - Support Remote NDIS (RNDIS) - Support WINUSB1.0、WINUSB2.0(with BOS) - Support Vendor class +- Support UF2 - Support multi device with the same USB IP CherryUSB Device Stack resource usage (GCC 10.2 with -O2): diff --git a/README_zh.md b/README_zh.md index eb804f3..59e623e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -67,6 +67,7 @@ CherryUSB Device 协议栈当前实现以下功能: - 支持 Remote NDIS (RNDIS) - 支持 WINUSB1.0、WINUSB2.0(带 BOS ) - 支持 Vendor 类 class +- 支持 UF2 - 支持相同 USB IP 的多从机 CherryUSB Device 协议栈资源占用说明(GCC 10.2 with -O2): diff --git a/demo/bootuf2/bootuf2.c b/demo/bootuf2/bootuf2.c new file mode 100644 index 0000000..b992c4b --- /dev/null +++ b/demo/bootuf2/bootuf2.c @@ -0,0 +1,430 @@ +/* + * Copyright (c) 2024, sakumisu + * Copyright (c) 2024, Egahp + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "bootuf2.h" +#include "usbd_core.h" + +char file_INFO[] = { + "CherryUSB UF2 BOOT\r\n" + "Model: " CONFIG_PRODUCT "\r\n" + "Board-ID: " CONFIG_BOARD "\r\n" +}; + +const char file_IDEX[] = { + "\n" + "" + "
" + "" + "" + "\n" +}; + +const char file_JOIN[] = { + "\n" + "" + "" + "" + "" + "\n" +}; + +const char file_ID__[12] = BOOTUF2_FAMILYID_ARRAY; + +static struct bootuf2_FILE files[] = { + [0] = { .Name = file_ID__, .Content = NULL, .FileSize = 0 }, + [1] = { .Name = "INFO_UF2TXT", .Content = file_INFO, .FileSize = sizeof(file_INFO) - 1 }, + [2] = { .Name = "INDEX HTM", .Content = file_IDEX, .FileSize = sizeof(file_IDEX) - 1 }, + [3] = { .Name = "JOIN HTM", .Content = file_JOIN, .FileSize = sizeof(file_JOIN) - 1 }, +}; + +/*!< define DBRs */ +static const struct bootuf2_DBR bootuf2_DBR = { + .JMPInstruction = { 0xEB, 0x3C, 0x90 }, + .OEM = "UF2 UF2 ", + .BPB = { + .BytesPerSector = CONFIG_BOOTUF2_SECTOR_SIZE, + .SectorsPerCluster = CONFIG_BOOTUF2_SECTOR_PER_CLUSTER, + .ReservedSectors = CONFIG_BOOTUF2_SECTOR_RESERVED, + .NumberOfFAT = CONFIG_BOOTUF2_NUM_OF_FAT, + .RootEntries = CONFIG_BOOTUF2_ROOT_ENTRIES, + .Sectors = (BOOTUF2_SECTORS(0) > 0xFFFF) ? 0 : BOOTUF2_SECTORS(0), + .MediaDescriptor = 0xF8, + .SectorsPerFAT = BOOTUF2_SECTORS_PER_FAT(0), + .SectorsPerTrack = 1, + .Heads = 1, + .HiddenSectors = 0, + .SectorsOver32MB = (BOOTUF2_SECTORS(0) > 0xFFFF) ? BOOTUF2_SECTORS(0) : 0, + .BIOSDrive = 0x80, + .Reserved = 0, + .ExtendBootSignature = 0x29, + .VolumeSerialNumber = 0x00420042, + .VolumeLabel = "CHERRYUF2", + .FileSystem = "FAT16 ", + }, +}; + +/*!< define mask */ +static uint8_t __attribute__((aligned(4))) bootuf2_mask[BOOTUF2_BLOCKSMAX / 8 + 1] = { 0 }; + +/*!< define state */ +static struct bootuf2_STATE bootuf2_STATE = { + .NumberOfBlock = 0, + .NumberOfWritten = 0, + .Mask = bootuf2_mask, + .Enable = 1, +}; + +/*!< define flash cache */ +static uint8_t __attribute__((aligned(4))) bootuf2_disk_cache[CONFIG_BOOTUF2_CACHE_SIZE]; + +/*!< define flash buff */ +static uint8_t __attribute__((aligned(4))) bootuf2_disk_fbuff[256]; + +/*!< define erase flag buff */ +static uint8_t __attribute__((aligned(4))) bootuf2_disk_erase[BOOTUF2_DIVCEIL(CONFIG_BOOTUF2_PAGE_COUNTMAX, 8)]; + +/*!< define disk */ +static struct bootuf2_data bootuf2_disk = { + .DBR = &bootuf2_DBR, + .STATE = &bootuf2_STATE, + .fbuff = bootuf2_disk_fbuff, + .erase = bootuf2_disk_erase, + .cache = bootuf2_disk_cache, + .cache_size = sizeof(bootuf2_disk_cache), +}; + +static void fname_copy(char *dst, char const *src, uint16_t len) +{ + for (size_t i = 0; i < len; ++i) { + if (*src) + *dst++ = *src++; + else + *dst++ = ' '; + } +} + +static void fcalculate_cluster(struct bootuf2_data *ctx) +{ + /*!< init files cluster */ + uint16_t cluster_beg = 2; + for (int i = 0; i < ARRAY_SIZE(files); i++) { + files[i].ClusterBeg = cluster_beg; + files[i].ClusterEnd = -1 + cluster_beg + + BOOTUF2_DIVCEIL(files[i].FileSize, + ctx->DBR->BPB.BytesPerSector * + ctx->DBR->BPB.SectorsPerCluster); + cluster_beg = files[i].ClusterEnd + 1; + } +} + +static int ffind_by_cluster(uint32_t cluster) +{ + if (cluster >= 0xFFF0) { + return -1; + } + + for (uint32_t i = 0; i < ARRAY_SIZE(files); i++) { + if ((files[i].ClusterBeg <= cluster) && + (cluster <= files[i].ClusterEnd)) { + return i; + } + } + + return -1; +} + +static bool uf2block_check_address(struct bootuf2_data *ctx, + struct bootuf2_BLOCK *uf2) +{ + uint32_t beg; + uint32_t end; + + beg = uf2->TargetAddress; + end = uf2->TargetAddress + uf2->PayloadSize; + + // if ((end >= beg) && (beg >= ctx->offset) && + // (end <= ctx->offset + ctx->size)) + // { + // return true; + // } + + return true; +} + +static bool bootuf2block_check_writable(struct bootuf2_STATE *STATE, + struct bootuf2_BLOCK *uf2, uint32_t block_max) +{ + if (uf2->NumberOfBlock) + { + if (uf2->BlockIndex < block_max) + { + uint8_t mask = 1 << (uf2->BlockIndex % 8); + uint32_t pos = uf2->BlockIndex / 8; + + if ((STATE->Mask[pos] & mask) == 0) + { + return true; + } + } + } + + return false; +} + +static void bootuf2block_state_update(struct bootuf2_STATE *STATE, + struct bootuf2_BLOCK *uf2, uint32_t block_max) +{ + if (uf2->NumberOfBlock) + { + if (STATE->NumberOfBlock != uf2->NumberOfBlock) + { + if ((uf2->NumberOfBlock >= BOOTUF2_BLOCKSMAX) || + STATE->NumberOfBlock) + { + /*!< uf2 block only can be update once */ + /*!< this will cause never auto reboot */ + STATE->NumberOfBlock = 0xffffffff; + } + else + { + STATE->NumberOfBlock = uf2->NumberOfBlock; + } + } + + if (uf2->BlockIndex < block_max) + { + uint8_t mask = 1 << (uf2->BlockIndex % 8); + uint32_t pos = uf2->BlockIndex / 8; + + if ((STATE->Mask[pos] & mask) == 0) + { + STATE->Mask[pos] |= mask; + STATE->NumberOfWritten++; + } + } + } + + USB_LOG_DBG("UF2 block total %d written %d index %d\r\n", + uf2->NumberOfBlock, STATE->NumberOfWritten, uf2->BlockIndex); +} + +static bool bootuf2block_state_check(struct bootuf2_STATE *STATE) +{ + return (STATE->NumberOfWritten >= STATE->NumberOfBlock) && + STATE->NumberOfBlock; +} + +void bootuf2_init(void) +{ + struct bootuf2_data *ctx; + + ctx = &bootuf2_disk; + + fcalculate_cluster(ctx); +} + +int boot2uf2_read_sector(uint32_t start_sector, uint8_t *buff, uint32_t sector_count) +{ + struct bootuf2_data *ctx; + + ctx = &bootuf2_disk; + + while (sector_count) { + memset(buff, 0, ctx->DBR->BPB.BytesPerSector); + + uint32_t sector_relative = start_sector; + + /*!< DBR sector */ + if (start_sector == BOOTUF2_SECTOR_DBR_END) { + memcpy(buff, ctx->DBR, sizeof(struct bootuf2_DBR)); + buff[510] = 0x55; + buff[511] = 0xaa; + } + /*!< FAT sector */ + else if (start_sector < BOOTUF2_SECTOR_FAT_END(ctx->DBR)) { + uint16_t *buff16 = (uint16_t *)buff; + + sector_relative -= BOOTUF2_SECTOR_RSVD_END(ctx->DBR); + + /*!< Perform the same operation on all FAT tables */ + while (sector_relative >= ctx->DBR->BPB.SectorsPerFAT) { + sector_relative -= ctx->DBR->BPB.SectorsPerFAT; + } + + uint16_t cluster_unused = files[ARRAY_SIZE(files) - 1].ClusterEnd + 1; + uint16_t cluster_absolute_first = sector_relative * + BOOTUF2_FAT16_PER_SECTOR(ctx->DBR); + + /*!< cluster used link to chain, or unsed */ + for (uint16_t i = 0, cluster_absolute = cluster_absolute_first; + i < BOOTUF2_FAT16_PER_SECTOR(ctx->DBR); + i++, cluster_absolute++) { + if (cluster_absolute >= cluster_unused) + buff16[i] = 0; + else + buff16[i] = cluster_absolute + 1; + } + + /*!< cluster 0 and 1 */ + if (sector_relative == 0) { + buff[0] = ctx->DBR->BPB.MediaDescriptor; + buff[1] = 0xff; + buff16[1] = 0xffff; + } + + /*!< cluster end of file */ + for (uint32_t i = 0; i < ARRAY_SIZE(files); i++) { + uint16_t cluster_file_last = files[i].ClusterEnd; + + if (cluster_file_last >= cluster_absolute_first) { + uint16_t idx = cluster_file_last - cluster_absolute_first; + if (idx < BOOTUF2_FAT16_PER_SECTOR(ctx->DBR)) { + buff16[idx] = 0xffff; + } + } + } + } + /*!< root entries */ + else if (start_sector < BOOTUF2_SECTOR_ROOT_END(ctx->DBR)) { + sector_relative -= BOOTUF2_SECTOR_FAT_END(ctx->DBR); + + struct bootuf2_ENTRY *ent = (void *)buff; + int remain_entries = BOOTUF2_ENTRY_PER_SECTOR(ctx->DBR); + + uint32_t file_index_first; + + /*!< volume label entry */ + if (sector_relative == 0) { + fname_copy(ent->Name, (char const *)ctx->DBR->BPB.VolumeLabel, 11); + ent->Attribute = 0x28; + ent++; + remain_entries--; + file_index_first = 0; + } else { + /*!< -1 to account for volume label in first sector */ + file_index_first = sector_relative * BOOTUF2_ENTRY_PER_SECTOR(ctx->DBR) - 1; + } + + for (uint32_t idx = file_index_first; + (remain_entries > 0) && (idx < ARRAY_SIZE(files)); + idx++, ent++) { + const uint32_t cluster_beg = files[idx].ClusterBeg; + + const struct bootuf2_FILE *f = &files[idx]; + + if ((0 == f->FileSize) && + (0 != idx)) { + continue; + } + + fname_copy(ent->Name, f->Name, 11); + ent->Attribute = 0x05; + ent->CreateTimeTeenth = BOOTUF2_SECONDS_INT % 2 * 100; + ent->CreateTime = BOOTUF2_DOS_TIME; + ent->CreateDate = BOOTUF2_DOS_DATE; + ent->LastAccessDate = BOOTUF2_DOS_DATE; + ent->FirstClustH16 = cluster_beg >> 16; + ent->UpdateTime = BOOTUF2_DOS_TIME; + ent->UpdateDate = BOOTUF2_DOS_DATE; + ent->FirstClustL16 = cluster_beg & 0xffff; + ent->FileSize = f->FileSize; + } + } + /*!< data */ + else if (start_sector < BOOTUF2_SECTOR_DATA_END(ctx->DBR)) { + sector_relative -= BOOTUF2_SECTOR_ROOT_END(ctx->DBR); + + int fid = ffind_by_cluster(2 + sector_relative / ctx->DBR->BPB.SectorsPerCluster); + + if (fid >= 0) { + const struct bootuf2_FILE *f = &files[fid]; + + uint32_t sector_relative_file = + sector_relative - + (files[fid].ClusterBeg - 2) * ctx->DBR->BPB.SectorsPerCluster; + + size_t fcontent_offset = sector_relative_file * ctx->DBR->BPB.BytesPerSector; + size_t fcontent_length = f->FileSize; + + if (fcontent_length > fcontent_offset) { + const void *src = (void *)((uint8_t *)(f->Content) + fcontent_offset); + size_t copy_size = fcontent_length - fcontent_offset; + + if (copy_size > ctx->DBR->BPB.BytesPerSector) { + copy_size = ctx->DBR->BPB.BytesPerSector; + } + + memcpy(buff, src, copy_size); + } + } + } + /*!< unknown sector, ignore */ + + start_sector++; + sector_count--; + buff += ctx->DBR->BPB.BytesPerSector; + } + + return 0; +} + +int bootuf2_write_sector(uint32_t start_sector, const uint8_t *buff, uint32_t sector_count) +{ + struct bootuf2_data *ctx; + + ctx = &bootuf2_disk; + + while (sector_count) { + struct bootuf2_BLOCK *uf2 = (void *)buff; + + if (!((uf2->MagicStart0 == BOOTUF2_MAGIC_START0) && + (uf2->MagicStart1 == BOOTUF2_MAGIC_START1) && + (uf2->MagicEnd == BOOTUF2_MAGIC_END) && + (uf2->Flags & BOOTUF2_FLAG_FAMILID_PRESENT) && + !(uf2->Flags & BOOTUF2_FLAG_NOT_MAIN_FLASH))) { + goto next; + } + + if (uf2->FamilyID == CONFIG_BOOTUF2_FAMILYID) { + if (bootuf2block_check_writable(ctx->STATE, uf2, CONFIG_BOOTUF2_FLASHMAX)) { + bootuf2_write_flash(ctx, uf2); + bootuf2block_state_update(ctx->STATE, uf2, CONFIG_BOOTUF2_FLASHMAX); + } else { + USB_LOG_DBG("UF2 block %d already written\r\n", + uf2->BlockIndex); + } + } + else { + USB_LOG_DBG("UF2 block illegal id %08x\r\n", uf2->FamilyID); + } + + next: + start_sector++; + sector_count--; + buff += ctx->DBR->BPB.BytesPerSector; + } + + return 0; +} + +uint16_t bootuf2_get_sector_size(void) +{ + return bootuf2_disk.DBR->BPB.BytesPerSector; +} + +uint32_t bootuf2_get_sector_count(void) +{ + return bootuf2_disk.DBR->BPB.SectorsOver32MB + bootuf2_disk.DBR->BPB.Sectors; +} + +bool bootuf2_is_write_done(void) +{ + return bootuf2block_state_check(&bootuf2_disk.STATE); +} \ No newline at end of file diff --git a/demo/bootuf2/bootuf2.h b/demo/bootuf2/bootuf2.h new file mode 100644 index 0000000..b6a6e16 --- /dev/null +++ b/demo/bootuf2/bootuf2.h @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2024, sakumisu + * Copyright (c) 2024, Egahp + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef BOOTUF2_H +#define BOOTUF2_H + +#include3kCtSu>EGJmZ^IALY&xmgx1ERBh|aCl+&!P?cw8uE;VqZ<_cfg#2`|{z(?=&3 zwX2csmz1cj*SKVzA%IcIzu4Gmd}@_QM@m@80<3Q3KcrT ~4M!^m4LO=vN^cmL1OH-8J9&-ph8+-mV}m^d!FCvPxq8f+oMj0+e5B zuS0GsuvfV2u|Q1h(|+>_KpF$)Xv>|`{kZyrT`vVR!_<8RC+CcZYhT%cJCYHmQV?!i z2ZVHxYKeEyc#}JIJY=>?rF%RoQJ(fEBQ`9ezu^s|9Pi7_{8TseD4Lf)+4&B2&?6ji z!rLK0%p$PIO%U#w?2fo&@b2lmd-?F$gMpT(WIWg{AuFG$Zq=a}Mc;$c$f?MS_mgnD z&V3uY`~&nY-HTB>_QL?myFjl4zSk?x<9!PrS3Je#K35U0`!zPmTI%MnQ~gVAe)?)! zuLX;9z?R->m9{=^ _8X4xLTFjhyP@=;T(qUvFKU?>ziJ=Cn^xSw%6V~p+hlWjk z@!=^e#ghR$FQ~Egd=NG_*1GUQ>wHXq_!6!qEVYs@Td8Ywtv;`McZ=+qVxEVf+JAl4 za6wn%+wexjc$j~#4+qY$OyKZn_QEg+et%3(qdnrSBT~S0GF}|`X*0Q~%fK@cN864= zN(cERfHA7IgV`Y5ZskJ 3GodG()s* zJtn1Be`-U~6lhloDCrB^noHB3krggLqxLpHFlT3`N))T>sIc8V?aN@JHq?cX-7% 4LVbAM`O~yAsD0Ah(PM81y|4N8wKNH><;m_UEK(5 zC`4xC5di2{>-RO^ae5M@Iqmf|wb-Lo%*FZQH13LMM3*zD!KrGI;c4UK(6l?k#Oj7W zA8Im{cGD-Fdd8sg-IHiNMV4F-yH7 wy`{crVXEmXyFiFB}~`~+3Ou`w(RrB;pFuV17+>_(18Y~w6SyK9?3d$fs% zm5b=?8p3oJY2kXm2It=wg*(ova~Sm0<5!awY%)8SX%J|l!Z!fX bUR+9oaFxE}Y*3wr-JdOAj zZZqG?2g&74(!L2=;!I0fnNCn+wn~&!Ex!v82-oLwH&~e6#n>x%(e;*iV@6Sy@Yvu3 zm?r}i|9(7XGHl%I;ev${&y5wn73QC#s0ZOcPN(BB(Z6*&u$no+e+#c(FxlU7sW;l; z^$6ek{$HH}neyH{lm62v5XpI;{GV3eNkjkE4Z;P|{L>K?=ifu&T )ca1)e zjZK+dH;bm3`l~HY2L}f@$2k*r?$C;Ahy5}C5#y9HLr7~!2a&~GnfT#-l2!Tg^f^aC zr!&2NPF>wcX=&+X0Uz(3&&7A^wmrzSX9F$mL+^#=jL`udFOV}32V0?4`H0WCAMrtr z_^+Xnk!jLh)y@Y@@5kl!tzHb-V~0f}0aX4wl1KusoLll+m(!JI*WCnF+a>eDJ}ahy z^%`R&@2k{x;B&{* >roQs<@e2_6DRyXo zHCrg?x=*IK4&JXzkTyukpV<3uGkLkeX8p9OLMxtuiD_V=+HgBjTzj*k^|U*P*t_V3 z5di3}pVuk%OYz+E*`?F%3ddz51C4lfoOL`prhmDi2DLpufMRxiUT%G&maSWV+ojYK z>^);J_#9*hU0W$&fxHqYNWhUVPvbAW0xwqrps=>N8pzqpGgAk++~%+o
|=CXTrP2sBil%o&x^Nq=F{q#caUj@8@$ic zHZl3_;Z^2~=ct2)F`tWq+k3ME37i~?qNhz%pI-^8Iw7N@QnEY=p)W3-kE>15q@5#C zxV}^7L$Q<#Rr*2_BZbnbfyIi2TOrgL6R8>q%PnA=VK~ zjW(Hv!4He$-UY BP!Xd;R@Wiv9lvNc&7bZr z4o*)a>Ep)bn6L$-f|N#o4>v2# 6^AtyaA=BUTSbx+p(yYxF>Q*W#TTac3wb7;8 zjUR4h$D!2V_qeh2(pUG5Q98}nxqiAD2W@<^UTNY_lD;9=zsm49oz#~;t*qPP3?LyL z@Bx)Kl66Tvqr86ALa^88x>)F+oG+Qw|9ZAB40{{8bmqa_h2ITOt3t%Y;<6~PTYqod z6beAZGkxe($|m;nlG#jADmtItp$dA6de%IRYR{js3^ *Y!gj zm9#y5T$^j5R7%cEVkukZ 3L%tpWA-5cy9HEJlMs%hrEt%T=&-0 z{gAjERcLvcOt0BMe|f8Jnw}40B=)+F+1{oEPlPdba89{X!3w{M2eZ_kO;aonHq@Y6 z*w)3Ky{-*pLg2C|E+6lC0E3)t<0q*0i)TV8cO)d0tgIM1uJf4=9TZ+AJ~8YzpLZjX zlP-p|V@}h)n?8?b28!3-dz`fgt+u*==sxQBDFM>HM%BHe)DV3YRX^LOMYSoU@%wmn zX(droE#{np+O6h1R^r}Rmt)mYamU7uEw&VWs%B>fk PCr$a1px`ml9-3#3!8J1@DcRg!mWL7mT_PRBkajU>jFgB<_I z=&;zD$Z+Yz2{O{*kB9+|rSWx%V!!a-ndIweV5YSkHOI)srl%7k=t8820kEX{&J*QD1Oh|dq&cy8mzEZ-b2VyKHk4UOW6@y`-n7~{LuT1BVC)JKp;&-2I4$|D1S znqtPAF=RMN6+$|WYeuf4qEGJ?iW8jRha0Sxuaj|TAOA?L-w`QpxSn(Ef>DM6cdDI! zl6x;1@K^)fwbf*ds#Q9tOr}C&Dvn=&|B=eUGpUZAYu1nAPHcf$DE`U%q{H0#B<{OG zWLOPUJFSj^H0Hvwr=DLmGNksBIcq70-{;L;IPAHRf*VRJJ2V6ej+Z#vDBsJsx3}Lx zcS;+tsbTz?GWA-8wc_wMu|K{~edPTRNAz@EDA4D&pI%69Z;Qwtk|@cAgoFg{q;i85 z8;PoWuRdaJLo3mnW-j|G8sCFXL6?Zl!vYKHw#t!bmZGPPPEfocvVBq4EJSB9Et75- z`x1@VR#zro6o>W3;aIX{^ E zbwpQxiQ<1K@+N>cP-!TwXpxZ3AxUs4l<>&eRTyLP+(dfNjeCuOOHA3-tKsY+DNnXH z>NBT8Kz*Z<*THLYGe_Ur5gB6tb9KTpq^1F0LvzG`5#2cU`|#p@V52`2sA}IbwqH;2 zM+L-Q2(l8H?Tx0y(2230Pjg$-V4~wG+^ 66BUv~H hp|*F`9TCb;QDY>&_R$`e|Q&}XJ4 zV< pGKQ{ zh>3~GrrU$#Vq3q6wbntWC}&`uI}4FWa9W93EFpcYkMoigi@V^OqWo>;ma}+VW}LM7 zb}kXqgM)(_{Ts!`f~HYgOZ|{n%pA2vSkfZ<*iwrj(Q;Z l6Z7%*ws`G z)wp2JS!t{u04xkjW~dD~f}Y>94{FmBdq{4@V8x{&ijPxP6CfA9p#?hJ{oQ(FsG`hh z;;I2tv%?{dg|R~@qv8~K!eSa($FRdz#9mw#>0DT%Cbz5AhN9izl@80K74^ ECwwvO0&~+&4T#<3nzZu0ck9mtiX!TwA|DgJuuBrJCy@`Z)?+~aHN&e#e z9M}I&Z83|$E1*MZE@VtUWe)xH;9!Nm#e1j*poLfOpOMJO{?kAB4_D)Vbt29`IGt~F zZ9lL$xwxP@PKE@g$@1U77I?mh0(Sz*#YII2^ss*fvSbL5<7cjK_Y5%cEuag3k`lMo zs5W?G@8Do}^_zoD><{^d{2`DgM}P3W+#e(i003-97#f-X%P;+SSy=RHSvINsUfh0v z2)s!c{a`Y@z&!BKdpRmLF(+qd6oukHvk?hm@zaJOyWERcl<9(UhXhID(SPOi%$}!_ zqf(*8wIKYDnC#h&zQ*Hb;pF}42GuJ*`s)30m-ur HU7rq Z;~Y# zrrp}(iNC;;uv2Rx0AGh6HA-1ZX@6TW+1~UzEi2p5MiB<&0LfUx$Ks6*&|Twt8~0eN z_o)ooJ-7y*20>;HtM~`10#7cnt3a7ZuGJwncI6Cl!=*vn<|ZQ!aSnSweu3u(e`XMf z4!&s+2+W;)o sxC0IJ{k~~$if%jxz{XqZH$JVqn#+CR+ zgm1$pEJ?rEcy(Ydz0lg{P3=x|{FV6ILfn=bX<+VjRuD}bZgQF6H VK7F o9=Qzix>`@O=QbxS*z*-lS!a)h*U|eLjRZ@JaO^v7T7e_Fo+Otk zFOECSmIJTqqv#lag9c(1bSXT& }mDIXeN17(sT#whvV~fBrc3WTEDo@%hdGd#&4I zK@)&Ih_aF1t_}!aRPlI*)s}Zo4C1kD1orbBCenmTJ?*C?{T8*fui!DF9BP;& zHO3(SX~yzS-l@rW2xs0CV`pVZfpnqLTNPHk#rNmvg+tUcQInbR69wFqveG;T%6qxP zA)aQj9Gy%6&8pJIit@Z!N{37)lAV&*>4Rix^|uqZjMj!Tt}%`Zli`yh%dMYAYXL zyl|TgB#2B_-G~~V4{(o>$0x2vw7&oD1Og%ZTT@&mQ-*{-cvkYRmgy|_ AVx-wWo-%V) z?2*qIY11GjM!B!n8J`>}xvXfG*7n||X#PGB!nW0VUvlf;syo_K?m|W6ZD-fVs+FCw zKb!U=g&rs81G21cT5P^=Y`cJE@~)7b&9u#V&`RazWqaip)UWLrN78jjE@i2(pZH*N zN7h-LG-2=+QUK5sB~r+h6$U|U#0RQ#N(Pi8KDB}`jTM0Q*WKD)R7u{*FI8 c(<{lD@Ya5vdI|~GQ4aUpNyTgNwo^5zhK?T3qy0K+z-=+!F0i%0LKX(> z92Fnu)2+3-o=>K$8CXcWZ%#UERjg63Fy;7r(DGe|yN~T(8@icu>IZ+Vdnsedpl$Xd zRB!4qAX{^je~witeCs$bz()nBNpv@Czrduw>th4R{XR^x$3Tm4##X(X^{{d6CNBDw z1E_N!9lmtVH5OwuIBB%pxP~w(UfK@XX&tlj3xzX!D|TcCy!BZK0R1kmvV-q5H3<%} z2$|XkVt3rhBwn;}S#I;H|FFt=C8<}CO~Na=AF)Tl=3r}K4TCgCR-l=2K@^+j^u!yZ z7hK~8+T>nE?hRQSfNUX}9|^#0e@$eW^w0IRK#wYh?s1 Fe9@ z=J|-Kj@hkG0iAPSqx=whonJ6$W$Do%TkB!7H`wb%H;p&A@h2x=7jG-Y>)`M#_ilp@ z9k%c=y+;=bw@sJpWmPurM>Ng#0x~rplQcn< N402=Z%)#xohF1^uA~tfwL}t=wXa5*->gcw1xS(Y&OpZXhb AAgl3&o>!+;)g$wXkrdUKv~TlSX3fjSFL5)WM;va<<-@Q zi|v8J+&|u7ucM |2KSd$M^5R zU=`*6%%J}F5&WRQ<9pYXomrJ6l*I!AXI|`OiH_03c`ZZ!z7g4_-b@3K@^Um6l?(@w z^yB-l9bf5ilsbcaW0rT;FSccS19Vfu5DGK+7y2mwP5~CWe~^ 9GD7|p82?$}!ZVBgNtDs# zB@0m)<9EP`>-7!%v7^Rbi}vx&+tAXBbT0MkFqFEDc>z%G=#~uc