Checkpoint EEPROM, firmware analysis and instrument protocol updates
This commit is contained in:
44
c/set-protocol/include/balsam167_eeprom_block.h
Normal file
44
c/set-protocol/include/balsam167_eeprom_block.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/* Private PM35 FC03 EEPROM window; not standard Modbus register quantities.
|
||||
* AT25256B keeps its existing 0x8000..0xBFFF / count 1..120 requests.
|
||||
* M95M02 uses four 64-KiB windows: count bit15 marks the profile, bits9:8
|
||||
* select bank 0..3, bits7:0 carry word count. Address 0x8000..0xFFFF is
|
||||
* the word offset within a bank. Every request is self-contained; no global
|
||||
* bank register can be changed by another client. Old firmware rejects the
|
||||
* marked quantity. Neither profile silently reads the other memory type.
|
||||
*/
|
||||
#ifndef BALSAM167_EEPROM_BLOCK_H
|
||||
#define BALSAM167_EEPROM_BLOCK_H
|
||||
#include "balsam167_eeprom_layout.h"
|
||||
#define BALSAM167_EEPROM_RTU_BASE 0x8000U
|
||||
#define BALSAM167_EEPROM_BLOCK_WORDS 120U
|
||||
#define BALSAM167_EEPROM_M95_MARKER 0x8000U
|
||||
#define BALSAM167_EEPROM_WRITE_M95 0x44U
|
||||
static unsigned long balsam167_eeprom_capacity(unsigned profile)
|
||||
{
|
||||
return profile == 0 ? 32768UL : (profile == 1 ? 262144UL : 0UL);
|
||||
}
|
||||
static unsigned long balsam167_eeprom_block_word(unsigned address, unsigned count)
|
||||
{
|
||||
return ((unsigned long)(count & 0x0300U) << 7) + (address - 0x8000U);
|
||||
}
|
||||
static unsigned balsam167_eeprom_block_valid_profile(unsigned fn, unsigned address,
|
||||
unsigned count, unsigned profile)
|
||||
{
|
||||
unsigned n = count & 255U;
|
||||
unsigned long word;
|
||||
if (fn != 3 || address < 0x8000U || !n || n > 120U) return 0;
|
||||
if (profile == 0) return count == n && address < 0xC000U && n <= 0xC000U-address;
|
||||
if (profile != 1 || (count & 0xFC00U) != 0x8000U) return 0;
|
||||
word = balsam167_eeprom_block_word(address, count);
|
||||
return n <= 0x10000UL-address && word+n <= 131072UL;
|
||||
}
|
||||
static unsigned balsam167_eeprom_block_valid(unsigned fn, unsigned address, unsigned count)
|
||||
{
|
||||
#if EEPROM_M95M02
|
||||
return balsam167_eeprom_block_valid_profile(fn, address, count, 1);
|
||||
#else
|
||||
return fn == 3 && address >= 0x8000U && address < 0xC000U &&
|
||||
count && count <= 120U && count <= 0xC000U-address;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
@@ -5,8 +5,20 @@
|
||||
*/
|
||||
#ifndef BALSAM167_EEPROM_LAYOUT_H
|
||||
#define BALSAM167_EEPROM_LAYOUT_H
|
||||
#define BALSAM167_EEPROM_BYTES 0x8000
|
||||
/* EEPROM_M95M02 is selected by the board configuration, never autodetected
|
||||
* by writing test patterns. Keep numeric constants usable by the TI linker. */
|
||||
#define BALSAM167_AT25256_BYTES 0x8000
|
||||
#define BALSAM167_M95M02_BYTES 0x40000
|
||||
#ifndef EEPROM_M95M02
|
||||
#define EEPROM_M95M02 0
|
||||
#endif
|
||||
#if EEPROM_M95M02
|
||||
#define BALSAM167_EEPROM_BYTES BALSAM167_M95M02_BYTES
|
||||
#define BALSAM167_EEPROM_PAGE_BYTES 256
|
||||
#else
|
||||
#define BALSAM167_EEPROM_BYTES BALSAM167_AT25256_BYTES
|
||||
#define BALSAM167_EEPROM_PAGE_BYTES 64
|
||||
#endif
|
||||
#define BALSAM167_EEPROM_PARAM_WORDS 129
|
||||
#define BALSAM167_EEPROM_WORDS (BALSAM167_EEPROM_BYTES / 2)
|
||||
/* Preserve the historical end-exclusive parameter boundary. The final word
|
||||
|
||||
@@ -91,6 +91,16 @@ PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_request(const void *state,
|
||||
PCAN_ABI_API int pcan_abi_periph28335_eeprom_accept(void *state,
|
||||
const uint8_t *reply, size_t size, uint8_t *output, size_t capacity);
|
||||
PCAN_ABI_API uint32_t pcan_abi_periph28335_eeprom_capacity(void);
|
||||
PCAN_ABI_API uint32_t pcan_abi_periph28335_eeprom_capacity_profile(unsigned profile);
|
||||
PCAN_ABI_API int pcan_abi_periph28335_eeprom_begin_profile(void *r, uint32_t c, uint32_t a, uint32_t n, unsigned profile);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_expected_profile(const void *r, unsigned profile);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_request_profile(const void *r, uint8_t *o, size_t n, unsigned profile);
|
||||
PCAN_ABI_API int pcan_abi_periph28335_eeprom_block_accept_profile(void *r, const uint8_t *p, size_t n, uint8_t *o, size_t cap, unsigned profile);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_write_profile(uint32_t controller, uint32_t destination, uint32_t source, uint32_t length, unsigned profile, uint8_t *out, size_t cap);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_expected(const void *state);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_request(const void *state, uint8_t *out, size_t cap);
|
||||
PCAN_ABI_API int pcan_abi_periph28335_eeprom_block_accept(void *state, const uint8_t *reply, size_t size, uint8_t *out, size_t cap);
|
||||
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_append_crc(
|
||||
const uint8_t *payload, size_t payload_size,
|
||||
uint8_t *output, size_t output_size);
|
||||
|
||||
@@ -50,6 +50,22 @@ size_t periph28335_eeprom_request(const periph28335_eeprom_reader *reader,
|
||||
int periph28335_eeprom_accept(periph28335_eeprom_reader *reader,
|
||||
const uint8_t *reply, size_t size, uint8_t *output, size_t capacity);
|
||||
|
||||
/* FC03 block reader uses the same state/physical byte range as legacy PEEK.
|
||||
* expected() is the exact response length. No state advance on CRC/size error. */
|
||||
size_t periph28335_eeprom_block_expected(const periph28335_eeprom_reader *r);
|
||||
size_t periph28335_eeprom_block_request(const periph28335_eeprom_reader *r, uint8_t *out, size_t cap);
|
||||
int periph28335_eeprom_block_accept(periph28335_eeprom_reader *r,
|
||||
const uint8_t *reply, size_t size, uint8_t *out, size_t cap);
|
||||
|
||||
/* Profile 0 = AT25256B, 1 = M95M02; stable additional API for every host port.
|
||||
* State remains four uint32_t fields. M95 requires block mode, no PEEK fallback. */
|
||||
uint32_t periph28335_eeprom_capacity_profile(unsigned profile);
|
||||
int periph28335_eeprom_begin_profile(periph28335_eeprom_reader *r, uint32_t c, uint32_t a, uint32_t n, unsigned profile);
|
||||
size_t periph28335_eeprom_block_expected_profile(const periph28335_eeprom_reader *r, unsigned profile);
|
||||
size_t periph28335_eeprom_block_request_profile(const periph28335_eeprom_reader *r, uint8_t *o, size_t n, unsigned profile);
|
||||
int periph28335_eeprom_block_accept_profile(periph28335_eeprom_reader *r, const uint8_t *p, size_t n, uint8_t *o, size_t cap, unsigned profile);
|
||||
size_t periph28335_eeprom_write_profile(uint32_t controller, uint32_t destination, uint32_t source, uint32_t length, unsigned profile, uint8_t *out, size_t cap);
|
||||
|
||||
typedef enum {
|
||||
PERIPH28335_OK = 0,
|
||||
PERIPH28335_ERROR_ARGUMENT = -1,
|
||||
|
||||
@@ -380,6 +380,26 @@ int pcan_abi_gui_parser_stats(const void *parser_storage, uint32_t *frames,
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t pcan_abi_periph28335_eeprom_block_expected(const void *s)
|
||||
{ return periph28335_eeprom_block_expected((const periph28335_eeprom_reader *)s); }
|
||||
size_t pcan_abi_periph28335_eeprom_block_request(const void *s, uint8_t *out, size_t cap)
|
||||
{ return periph28335_eeprom_block_request((const periph28335_eeprom_reader *)s,out,cap); }
|
||||
int pcan_abi_periph28335_eeprom_block_accept(void *s, const uint8_t *reply, size_t size, uint8_t *out, size_t cap)
|
||||
{ return periph28335_eeprom_block_accept((periph28335_eeprom_reader *)s,reply,size,out,cap); }
|
||||
|
||||
uint32_t pcan_abi_periph28335_eeprom_capacity_profile(unsigned p)
|
||||
{ return periph28335_eeprom_capacity_profile(p); }
|
||||
int pcan_abi_periph28335_eeprom_begin_profile(void *s, uint32_t c, uint32_t a, uint32_t n, unsigned p)
|
||||
{ return periph28335_eeprom_begin_profile((periph28335_eeprom_reader *)s,c,a,n,p); }
|
||||
size_t pcan_abi_periph28335_eeprom_block_expected_profile(const void *s, unsigned p)
|
||||
{ return periph28335_eeprom_block_expected_profile((const periph28335_eeprom_reader *)s,p); }
|
||||
size_t pcan_abi_periph28335_eeprom_block_request_profile(const void *s, uint8_t *o, size_t n, unsigned p)
|
||||
{ return periph28335_eeprom_block_request_profile((const periph28335_eeprom_reader *)s,o,n,p); }
|
||||
int pcan_abi_periph28335_eeprom_block_accept_profile(void *s, const uint8_t *r, size_t n, uint8_t *o, size_t cap, unsigned p)
|
||||
{ return periph28335_eeprom_block_accept_profile((periph28335_eeprom_reader *)s,r,n,o,cap,p); }
|
||||
size_t pcan_abi_periph28335_eeprom_write_profile(uint32_t c, uint32_t d, uint32_t s, uint32_t n, unsigned p, uint8_t *o, size_t cap)
|
||||
{ return periph28335_eeprom_write_profile(c,d,s,n,p,o,cap); }
|
||||
|
||||
int pcan_abi_tms2812_bus_step(int write, unsigned step, unsigned tetrad, unsigned reg,
|
||||
uint16_t data, uint32_t *address, uint16_t *value, uint8_t *command)
|
||||
{
|
||||
|
||||
@@ -9,22 +9,24 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "set_crc.h"
|
||||
#include "balsam167_eeprom_block.h"
|
||||
|
||||
static int eeprom_reader_valid(const periph28335_eeprom_reader *r, unsigned profile)
|
||||
|
||||
static int eeprom_reader_valid(const periph28335_eeprom_reader *r)
|
||||
{
|
||||
return r && r->controller >= 1U && r->controller <= 255U &&
|
||||
r->start < PERIPH28335_EEPROM_BYTES && r->length &&
|
||||
r->length <= PERIPH28335_EEPROM_BYTES - r->start && r->received < r->length;
|
||||
r->start < balsam167_eeprom_capacity(profile) && r->length &&
|
||||
r->length <= balsam167_eeprom_capacity(profile) - r->start && r->received < r->length;
|
||||
}
|
||||
|
||||
int periph28335_eeprom_begin(periph28335_eeprom_reader *r,
|
||||
uint32_t controller, uint32_t start, uint32_t length)
|
||||
int periph28335_eeprom_begin_profile(periph28335_eeprom_reader *r,
|
||||
uint32_t controller, uint32_t start, uint32_t length, unsigned profile)
|
||||
{
|
||||
if (!r) return PERIPH28335_ERROR_ARGUMENT;
|
||||
/* Invalidate first: a rejected new range must not leave an old read active. */
|
||||
memset(r, 0, sizeof(*r));
|
||||
if (!controller || controller > 255U || start >= PERIPH28335_EEPROM_BYTES ||
|
||||
!length || length > PERIPH28335_EEPROM_BYTES - start)
|
||||
if (!controller || controller > 255U || start >= balsam167_eeprom_capacity(profile) ||
|
||||
!length || length > balsam167_eeprom_capacity(profile) - start)
|
||||
return PERIPH28335_ERROR_RANGE;
|
||||
r->controller = controller; r->start = start; r->length = length;
|
||||
return PERIPH28335_OK;
|
||||
@@ -35,7 +37,7 @@ size_t periph28335_eeprom_request(const periph28335_eeprom_reader *r,
|
||||
{
|
||||
uint8_t payload[6];
|
||||
uint32_t word;
|
||||
if (!eeprom_reader_valid(r)) return 0;
|
||||
if (!eeprom_reader_valid(r, 0)) return 0;
|
||||
word = (r->start + r->received) / 2U;
|
||||
payload[0] = (uint8_t)r->controller; payload[1] = 56U; /* CMD_PEEK only */
|
||||
/* 167 routes words 0..0x3fff to SPI EEPROM. The old terminal's
|
||||
@@ -49,7 +51,7 @@ int periph28335_eeprom_accept(periph28335_eeprom_reader *r,
|
||||
const uint8_t *reply, size_t size, uint8_t *output, size_t capacity)
|
||||
{
|
||||
unsigned odd, count;
|
||||
if (!eeprom_reader_valid(r) || !reply || !output) return PERIPH28335_ERROR_ARGUMENT;
|
||||
if (!eeprom_reader_valid(r, 0) || !reply || !output) return PERIPH28335_ERROR_ARGUMENT;
|
||||
/* The UART layer may strip two legacy zero-padding bytes after CRC. */
|
||||
if (size != 8U && size != 10U) return PERIPH28335_ERROR_LENGTH;
|
||||
if (periph28335_crc16_modbus(reply, 8U) != 0) return PERIPH28335_ERROR_CRC;
|
||||
@@ -64,6 +66,87 @@ int periph28335_eeprom_accept(periph28335_eeprom_reader *r,
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
/* One common codec for all host ports. Physical EEPROM bytes already have
|
||||
* network-word order; only trim an odd first/last byte, never swap them. */
|
||||
static unsigned eeprom_block_words(const periph28335_eeprom_reader *r, unsigned profile)
|
||||
{
|
||||
uint32_t n;
|
||||
if (!eeprom_reader_valid(r, profile)) return 0;
|
||||
n = (r->length - r->received + ((r->start + r->received) & 1U) + 1U) / 2U;
|
||||
/* A response cannot cross a bank boundary; trim the last block there. */
|
||||
if (n > 32768U - ((r->start+r->received)/2U % 32768U))
|
||||
n = 32768U - ((r->start+r->received)/2U % 32768U);
|
||||
return (unsigned)(n > BALSAM167_EEPROM_BLOCK_WORDS ? BALSAM167_EEPROM_BLOCK_WORDS : n);
|
||||
}
|
||||
size_t periph28335_eeprom_block_expected_profile(const periph28335_eeprom_reader *r, unsigned profile)
|
||||
{
|
||||
unsigned n = eeprom_block_words(r, profile);
|
||||
return n ? 5U + 2U*n : 0;
|
||||
}
|
||||
size_t periph28335_eeprom_block_request_profile(const periph28335_eeprom_reader *r, uint8_t *out, size_t cap, unsigned profile)
|
||||
{
|
||||
uint8_t p[6];
|
||||
unsigned n = eeprom_block_words(r, profile), a, quantity;
|
||||
uint32_t word;
|
||||
if (!n) return 0;
|
||||
word = (r->start + r->received) / 2U;
|
||||
a = 0x8000U + (unsigned)(word % 32768U);
|
||||
quantity = n | (profile ? (0x8000U | (unsigned)(word/32768U)*256U) : 0U);
|
||||
if (!balsam167_eeprom_block_valid_profile(3,a,quantity,profile)) return 0;
|
||||
p[0]=(uint8_t)r->controller; p[1]=3;
|
||||
p[2]=(uint8_t)(a>>8); p[3]=(uint8_t)a; p[4]=(uint8_t)(quantity>>8); p[5]=(uint8_t)quantity;
|
||||
return periph28335_append_crc(p,6,out,cap);
|
||||
}
|
||||
int periph28335_eeprom_block_accept_profile(periph28335_eeprom_reader *r,
|
||||
const uint8_t *reply, size_t size, uint8_t *out, size_t cap, unsigned profile)
|
||||
{
|
||||
unsigned n=eeprom_block_words(r, profile), odd, count;
|
||||
if (!n || !reply || !out) return PERIPH28335_ERROR_ARGUMENT;
|
||||
if (size != 5U+2U*n) return PERIPH28335_ERROR_LENGTH;
|
||||
if (periph28335_crc16_modbus(reply,size)) return PERIPH28335_ERROR_CRC;
|
||||
/* Common address 16 is answered by the board's own Mode, 5 or 6. */
|
||||
if ((reply[0] != r->controller && !(r->controller==16 && (reply[0]==5 || reply[0]==6))) ||
|
||||
reply[1]!=3 || reply[2]!=2U*n) return PERIPH28335_ERROR_HEADER;
|
||||
odd=(unsigned)((r->start+r->received)&1U);
|
||||
count=2U*n-odd;
|
||||
if (count>r->length-r->received) count=(unsigned)(r->length-r->received);
|
||||
if (cap<count) return PERIPH28335_ERROR_CAPACITY;
|
||||
memcpy(out,reply+3+odd,count);
|
||||
r->received+=count;
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
/* Keep all existing exported symbols and the 16-byte reader ABI unchanged. */
|
||||
int periph28335_eeprom_begin(periph28335_eeprom_reader *r, uint32_t c, uint32_t a, uint32_t n)
|
||||
{ return periph28335_eeprom_begin_profile(r,c,a,n,0); }
|
||||
size_t periph28335_eeprom_block_expected(const periph28335_eeprom_reader *r)
|
||||
{ return periph28335_eeprom_block_expected_profile(r,0); }
|
||||
size_t periph28335_eeprom_block_request(const periph28335_eeprom_reader *r, uint8_t *o, size_t n)
|
||||
{ return periph28335_eeprom_block_request_profile(r,o,n,0); }
|
||||
int periph28335_eeprom_block_accept(periph28335_eeprom_reader *r, const uint8_t *p, size_t n, uint8_t *o, size_t cap)
|
||||
{ return periph28335_eeprom_block_accept_profile(r,p,n,o,cap,0); }
|
||||
uint32_t periph28335_eeprom_capacity_profile(unsigned profile)
|
||||
{ return (uint32_t)balsam167_eeprom_capacity(profile); }
|
||||
/* Legacy EXTEND carries word addresses and octet lengths. A distinct M95
|
||||
* subcommand makes a wrong-profile write fail closed on either firmware. */
|
||||
size_t periph28335_eeprom_write_profile(uint32_t controller, uint32_t destination,
|
||||
uint32_t source, uint32_t length, unsigned profile, uint8_t *out, size_t cap)
|
||||
{
|
||||
uint8_t p[16]; unsigned i;
|
||||
uint32_t capacity = periph28335_eeprom_capacity_profile(profile);
|
||||
uint32_t limit = capacity ? capacity-(BALSAM167_EEPROM_PARAM_WORDS+1U)*2U : 0;
|
||||
if (!controller || controller>255U || !length || length>199993U ||
|
||||
destination>=limit/2U || length>limit-destination*2U) return 0;
|
||||
p[0]=(uint8_t)controller; p[1]=60;
|
||||
for(i=0;i<4;i++) {
|
||||
p[2+i]=(uint8_t)(destination>>(8*i));
|
||||
p[6+i]=(uint8_t)(source>>(8*i));
|
||||
p[10+i]=(uint8_t)(length>>(8*i));
|
||||
}
|
||||
p[14]=(uint8_t)(profile ? BALSAM167_EEPROM_WRITE_M95 : 4); p[15]=0;
|
||||
return periph28335_append_crc(p,16,out,cap);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *project;
|
||||
const char *commands[PERIPH28335_COMMAND_COUNT];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "periph28335.h"
|
||||
#include <assert.h>
|
||||
#include "balsam167_eeprom_block.h"
|
||||
#include <string.h>
|
||||
|
||||
static void response(uint8_t *out, unsigned high, unsigned low)
|
||||
@@ -12,9 +13,100 @@ static void response(uint8_t *out, unsigned high, unsigned low)
|
||||
out[8]=out[9]=0;
|
||||
}
|
||||
|
||||
static void blocks(void)
|
||||
{
|
||||
periph28335_eeprom_reader r;
|
||||
uint8_t req[8], reply[245], out[240];
|
||||
unsigned start, transactions, n, a, i, old;
|
||||
int got;
|
||||
assert(!balsam167_eeprom_block_valid(6,0x8000,1));
|
||||
assert(!balsam167_eeprom_block_valid(3,0xbfff,2));
|
||||
assert(!balsam167_eeprom_block_valid(3,0xffff,120));
|
||||
assert(!balsam167_eeprom_block_valid(3,0x8000,121));
|
||||
assert(!balsam167_eeprom_block_valid(3,0x8000,0));
|
||||
assert(balsam167_eeprom_block_valid(3,0xbfff,1));
|
||||
for(start=0;start<2;start++) {
|
||||
assert(!periph28335_eeprom_begin(&r,16,start,32768-start));
|
||||
transactions=0;
|
||||
while(r.received<r.length) {
|
||||
assert(periph28335_eeprom_block_request(&r,req,8)==8);
|
||||
assert(!periph28335_crc16_modbus(req,8));
|
||||
a=((unsigned)req[2]*256+req[3]-0x8000)*2;
|
||||
n=req[5]; assert(n && n<=120 && a+2*n<=32768);
|
||||
reply[0]=5; reply[1]=3; reply[2]=(uint8_t)(2*n);
|
||||
for(i=0;i<2*n;i++) reply[3+i]=(uint8_t)(a+i);
|
||||
assert(periph28335_append_crc(reply,3+2*n,reply,sizeof reply)==5+2*n);
|
||||
old=r.received;
|
||||
reply[4]^=1;
|
||||
assert(periph28335_eeprom_block_accept(&r,reply,5+2*n,out,240)==PERIPH28335_ERROR_CRC);
|
||||
assert(r.received==old); reply[4]^=1;
|
||||
assert(periph28335_eeprom_block_accept(&r,reply,5+2*n,out,0)==PERIPH28335_ERROR_CAPACITY);
|
||||
assert(r.received==old);
|
||||
got=periph28335_eeprom_block_accept(&r,reply,5+2*n,out,240);
|
||||
assert(got>0);
|
||||
for(i=0;i<(unsigned)got;i++) assert(out[i]==(uint8_t)(start+old+i));
|
||||
transactions++;
|
||||
}
|
||||
assert(transactions==137);
|
||||
assert(!periph28335_eeprom_block_request(&r,req,8));
|
||||
}
|
||||
}
|
||||
|
||||
static void profiles(void)
|
||||
{
|
||||
periph28335_eeprom_reader r;
|
||||
uint8_t req[18], reply[245], out[240];
|
||||
unsigned n, i, q, a, bankmask=0;
|
||||
uint32_t pos, before;
|
||||
assert(periph28335_eeprom_capacity_profile(0)==32768);
|
||||
assert(periph28335_eeprom_capacity_profile(1)==262144);
|
||||
assert(periph28335_eeprom_capacity_profile(2)==0);
|
||||
assert(periph28335_eeprom_begin_profile(&r,16,262143,2,1)<0);
|
||||
assert(periph28335_eeprom_begin_profile(&r,16,0,2,2)<0);
|
||||
assert(!balsam167_eeprom_block_valid_profile(3,0xffff,0x8078,1));
|
||||
assert(!balsam167_eeprom_block_valid_profile(3,0x8000,0x8401,1));
|
||||
assert(!balsam167_eeprom_block_valid_profile(3,0x8000,0x8001,0));
|
||||
assert(!balsam167_eeprom_block_valid_profile(3,0x8000,1,1));
|
||||
assert(balsam167_eeprom_block_valid_profile(3,0xffff,0x8301,1));
|
||||
/* Full 256-KiB dump from an odd start exercises every bank transition,
|
||||
* first/last byte trimming, CRC failure and exact physical byte order. */
|
||||
assert(!periph28335_eeprom_begin_profile(&r,16,1,262143,1));
|
||||
while(r.received<r.length) {
|
||||
assert(periph28335_eeprom_block_request_profile(&r,req,8,1)==8);
|
||||
assert(!periph28335_crc16_modbus(req,8));
|
||||
a=req[2]*256U+req[3]; q=req[4]*256U+req[5]; n=q&255U;
|
||||
assert(balsam167_eeprom_block_valid_profile(3,a,q,1));
|
||||
pos=balsam167_eeprom_block_word(a,q)*2UL;
|
||||
assert(pos == (r.start+r.received)/2*2);
|
||||
bankmask |= 1U << ((q>>8)&3U);
|
||||
assert(periph28335_eeprom_block_expected_profile(&r,1)==5+2*n);
|
||||
reply[0]=5; reply[1]=3; reply[2]=(uint8_t)(n*2);
|
||||
for(i=0;i<n*2;i++) reply[3+i]=(uint8_t)((pos+i)^((pos+i)>>16));
|
||||
periph28335_append_crc(reply,3+n*2,reply,sizeof(reply));
|
||||
before=r.received; reply[4]^=1;
|
||||
assert(periph28335_eeprom_block_accept_profile(&r,reply,5+2*n,out,240,1)==PERIPH28335_ERROR_CRC);
|
||||
assert(r.received==before); reply[4]^=1;
|
||||
assert(periph28335_eeprom_block_accept_profile(&r,reply,5+2*n,out,240,1)>0);
|
||||
for(i=0;i<r.received-before;i++) {
|
||||
pos=1+before+i;
|
||||
assert(out[i]==(uint8_t)(pos^(pos>>16)));
|
||||
}
|
||||
}
|
||||
assert(bankmask==15);
|
||||
assert(!periph28335_eeprom_block_request_profile(&r,req,8,1));
|
||||
assert(periph28335_eeprom_write_profile(5,0,0x200000,33514,0,req,18)==0);
|
||||
assert(periph28335_eeprom_write_profile(5,0,0x200000,33514,1,req,18)==18);
|
||||
assert(req[14]==0x44 && req[15]==0 && !periph28335_crc16_modbus(req,18));
|
||||
assert(!periph28335_eeprom_write_profile(5,0x1ff7e,0,2,1,req,18));
|
||||
assert(!periph28335_eeprom_write_profile(5,0xffffffff,0,2,1,req,18));
|
||||
assert(periph28335_eeprom_write_profile(5,0,0,32438,0,req,18)==18 && req[14]==4);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
periph28335_eeprom_reader reader;
|
||||
blocks();
|
||||
profiles();
|
||||
uint8_t request[8], reply[10], data[2];
|
||||
unsigned i;
|
||||
assert(sizeof(reader)==16);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Tektronix DPO4034 / MDO4034C
|
||||
# Tektronix DPO4034 / DPO4034B / MDO4034C
|
||||
|
||||
Независимый от Qt драйвер: `python/set_devices/tektronix.py`.
|
||||
Установка: `pip install "setcorp-templates[tektronix]"` из пакета templates.
|
||||
@@ -18,7 +18,7 @@
|
||||
## Реализованный обмен
|
||||
|
||||
Команды SCPI с LF. После открытия `*IDN?` проверяет производителя и точную
|
||||
модель DPO4034 или MDO4034C; `HEADER OFF` отключает заголовки ответов.
|
||||
модель DPO4034, DPO4034B или MDO4034C; `HEADER OFF` отключает заголовки ответов.
|
||||
|
||||
Для снимка запрашивается `ACQUIRE:STATE?`, затем отправляется
|
||||
`ACQUIRE:STATE STOP` и ожидается `*OPC?`. Это позволяет читать несколько
|
||||
|
||||
@@ -20,20 +20,42 @@ def _api(name, args, result):
|
||||
return function
|
||||
|
||||
|
||||
def capacity():
|
||||
return int(_api("capacity", [], ct.c_uint32)())
|
||||
def capacity(m95m02=False):
|
||||
return int(_api("capacity_profile", [ct.c_uint], ct.c_uint32)(int(m95m02)))
|
||||
|
||||
|
||||
def write_request(controller, destination, source, length, *, m95m02=False):
|
||||
values = (controller, destination, source, length)
|
||||
if any(type(v) is not int or not 0 <= v <= 0xffffffff for v in values):
|
||||
raise ValueError("Адрес или длина вне диапазона uint32")
|
||||
output = ct.create_string_buffer(18)
|
||||
size = _api("write_profile", [ct.c_uint32]*4 + [ct.c_uint, ct.c_void_p, ct.c_size_t], ct.c_size_t)(
|
||||
*values, int(m95m02), output, len(output))
|
||||
if not size:
|
||||
raise ValueError("LengthLoad или адрес выходит за область прошивки выбранной EEPROM либо пересекает область параметров. "
|
||||
"Предел от адреса 0: AT25256B — 32508 байт, M95M02 — 261884 байт.")
|
||||
return output.raw[:size]
|
||||
|
||||
|
||||
class Reader:
|
||||
def __init__(self, controller, start, length):
|
||||
def __init__(self, controller, start, length, *, block=False, m95m02=False):
|
||||
self.m95m02 = bool(m95m02)
|
||||
if m95m02 and not block:
|
||||
raise ValueError("Для M95M02 необходимо блочное чтение")
|
||||
self.block = block
|
||||
for value in (controller, start, length):
|
||||
if type(value) is not int or not 0 <= value <= 0xffffffff:
|
||||
raise ValueError("Адрес и длина должны быть неотрицательными целыми числами")
|
||||
self._state = _State()
|
||||
result = _api("begin", [ct.c_void_p, ct.c_uint32, ct.c_uint32, ct.c_uint32], ct.c_int)(
|
||||
ct.byref(self._state), controller, start, length)
|
||||
result = _api("begin_profile", [ct.c_void_p, ct.c_uint32, ct.c_uint32, ct.c_uint32, ct.c_uint], ct.c_int)(
|
||||
ct.byref(self._state), controller, start, length, int(self.m95m02))
|
||||
if result:
|
||||
raise ValueError("Диапазон вне EEPROM AT25256B: 0…0x7FFF байт; длина должна быть больше нуля")
|
||||
raise ValueError(f"Диапазон вне выбранной EEPROM: 0…0x{capacity(self.m95m02)-1:X}; длина должна быть больше нуля")
|
||||
if block:
|
||||
# Detect an old DLL before the GUI acquires ownership of the port.
|
||||
_api("block_request_profile", [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_uint], ct.c_size_t)
|
||||
_api("block_accept_profile", [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t, ct.c_uint], ct.c_int)
|
||||
self.expected
|
||||
|
||||
@property
|
||||
def received(self): return int(self._state.received)
|
||||
@@ -42,17 +64,26 @@ class Reader:
|
||||
@property
|
||||
def start(self): return int(self._state.start)
|
||||
|
||||
@property
|
||||
def expected(self):
|
||||
if not self.block: return 8
|
||||
return int(_api("block_expected_profile", [ct.c_void_p, ct.c_uint], ct.c_size_t)(ct.byref(self._state), int(self.m95m02)))
|
||||
|
||||
def request(self):
|
||||
output = ct.create_string_buffer(8)
|
||||
size = _api("request", [ct.c_void_p, ct.c_void_p, ct.c_size_t], ct.c_size_t)(
|
||||
ct.byref(self._state), output, len(output))
|
||||
args = [ct.c_void_p, ct.c_void_p, ct.c_size_t] + ([ct.c_uint] if self.block else [])
|
||||
extra = [int(self.m95m02)] if self.block else []
|
||||
size = _api("block_request_profile" if self.block else "request", args, ct.c_size_t)(
|
||||
ct.byref(self._state), output, len(output), *extra)
|
||||
return output.raw[:size]
|
||||
|
||||
def accept(self, reply):
|
||||
output = ct.create_string_buffer(2)
|
||||
output = ct.create_string_buffer(240 if self.block else 2)
|
||||
reply = bytes(reply)
|
||||
size = _api("accept", [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t], ct.c_int)(
|
||||
ct.byref(self._state), reply, len(reply), output, len(output))
|
||||
args = [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t] + ([ct.c_uint] if self.block else [])
|
||||
extra = [int(self.m95m02)] if self.block else []
|
||||
size = _api("block_accept_profile" if self.block else "accept", args, ct.c_int)(
|
||||
ct.byref(self._state), reply, len(reply), output, len(output), *extra)
|
||||
if size < 0:
|
||||
errors = {-3: "Неверная длина ответа EEPROM", -4: "Ошибка CRC ответа EEPROM",
|
||||
-5: "Неверный адрес контроллера или команда ответа EEPROM"}
|
||||
|
||||
@@ -8,6 +8,6 @@ Shared PySide6 terminal migrated from PySetTerminal-master/pysetterminal.
|
||||
- COM transport uses QtSerialPort. Embedded operations reuse the TMS2812 connection and configured controller address.
|
||||
- PEEK/POKE packet construction and response validation use the existing protocan native C ABI, with 16-bit TMS2812 values.
|
||||
- Macro files are stored under the application data directory, outside the source checkout.
|
||||
- PySide2 and pyserial are no longer required. A native set-protocol runtime remains required.
|
||||
- The standalone launcher uses PySide6; SETGUI Windows 7 uses its existing PySide2/Qt5 compatibility layer. Frozen SETGUI builds bundle Python and Qt. Pyserial is not required. A native set-protocol runtime remains required.
|
||||
|
||||
`build_and_clean.py` is the migrated original standalone packaging helper.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QGroupBox, QGridLayout, QLineEdit, QPushButton, QScrollArea
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QGroupBox, QGridLayout, QLineEdit, QPushButton, QScrollArea, QSizePolicy
|
||||
|
||||
def build_byte_page(self):
|
||||
self.byte_page = QScrollArea()
|
||||
@@ -13,18 +14,22 @@ def build_byte_page(self):
|
||||
panels = QHBoxLayout()
|
||||
for write in (True, False):
|
||||
group = QGroupBox("Запись" if write else "Чтение")
|
||||
group.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
||||
form = QGridLayout(group)
|
||||
address = QLineEdit("$1000")
|
||||
value = QLineEdit("0000" if write else "")
|
||||
binary = QLineEdit("0000 0000 0000 0000" if write else "")
|
||||
binary.setMinimumWidth(220)
|
||||
address.setFixedWidth(150)
|
||||
value.setFixedWidth(100)
|
||||
binary.setFixedWidth(240)
|
||||
value.setReadOnly(not write)
|
||||
binary.setReadOnly(not write)
|
||||
for row, (title, widget) in enumerate((("Адрес", address), ("Байт (hex)", value), ("Байт (bin)", binary))):
|
||||
form.addWidget(QLabel(title), row * 2, 0)
|
||||
form.addWidget(widget, row * 2 + 1, 0)
|
||||
form.addWidget(QLabel(title), row, 0)
|
||||
form.addWidget(widget, row, 1)
|
||||
button = QPushButton("Записать" if write else "Прочитать")
|
||||
form.addWidget(button, 6, 0)
|
||||
form.addWidget(button, 3, 1, Qt.AlignLeft)
|
||||
form.setColumnStretch(2, 1)
|
||||
if write:
|
||||
self.byte_write_address, self.byte_write_value, self.byte_write_binary = address, value, binary
|
||||
value.textEdited.connect(lambda text: self._sync_byte_value(text, 16))
|
||||
@@ -34,6 +39,7 @@ def build_byte_page(self):
|
||||
self.byte_read_address, self.byte_read_value, self.byte_read_binary = address, value, binary
|
||||
button.clicked.connect(lambda: self._start_byte(False))
|
||||
panels.addWidget(group)
|
||||
panels.addStretch()
|
||||
layout.addLayout(panels)
|
||||
status = QLabel("Нет данных от контроллера")
|
||||
status.setWordWrap(True)
|
||||
|
||||
@@ -229,6 +229,7 @@ class RawProtocolWidget(QWidget):
|
||||
self.mem_result_label = QLabel("---")
|
||||
mem_layout.addWidget(self.mem_result_label, 1, 3)
|
||||
|
||||
mem_layout.setColumnStretch(4, 1)
|
||||
mem_group.setLayout(mem_layout)
|
||||
layout.addWidget(mem_group)
|
||||
|
||||
@@ -238,6 +239,7 @@ class RawProtocolWidget(QWidget):
|
||||
|
||||
self.raw_send_edit = QLineEdit()
|
||||
self.raw_send_edit.setPlaceholderText("Введите hex данные для отправки")
|
||||
self.raw_send_edit.setMaximumWidth(640)
|
||||
raw_layout.addWidget(self.raw_send_edit)
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
@@ -250,6 +252,7 @@ class RawProtocolWidget(QWidget):
|
||||
hbox.addWidget(self.raw_read_btn)
|
||||
|
||||
|
||||
hbox.addStretch()
|
||||
raw_layout.addLayout(hbox)
|
||||
|
||||
self.raw_output = QTextEdit()
|
||||
@@ -260,7 +263,7 @@ class RawProtocolWidget(QWidget):
|
||||
|
||||
self.raw_clear_btn = QPushButton("Очистить логи")
|
||||
self.raw_clear_btn.clicked.connect(self.clear_logs)
|
||||
raw_layout.addWidget(self.raw_clear_btn)
|
||||
raw_layout.addWidget(self.raw_clear_btn, 0, Qt.AlignLeft)
|
||||
|
||||
raw_group.setLayout(raw_layout)
|
||||
layout.addWidget(raw_group)
|
||||
|
||||
@@ -1,11 +1,56 @@
|
||||
import json
|
||||
import os
|
||||
import weakref
|
||||
from functools import partial
|
||||
from copy import deepcopy
|
||||
from PySide6.QtWidgets import *
|
||||
from PySide6.QtCore import *
|
||||
from PySide6.QtGui import *
|
||||
|
||||
|
||||
class _MacroActionIcon(QIconEngine):
|
||||
"""Scalable action icons, painted with the button's current theme color."""
|
||||
def __init__(self, action, button):
|
||||
super().__init__()
|
||||
self.action = action
|
||||
self.button = weakref.ref(button)
|
||||
|
||||
def clone(self):
|
||||
return _MacroActionIcon(self.action, self.button())
|
||||
|
||||
def paint(self, painter, rect, mode, state):
|
||||
button = self.button()
|
||||
palette = button.palette() if button is not None else QApplication.palette()
|
||||
group = QPalette.Disabled if mode == QIcon.Disabled else QPalette.Active
|
||||
painter.save()
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
side = min(rect.width(), rect.height())
|
||||
painter.translate(rect.x() + (rect.width() - side) / 2, rect.y() + (rect.height() - side) / 2)
|
||||
painter.scale(side / 24, side / 24)
|
||||
painter.setPen(QPen(palette.color(group, QPalette.ButtonText), 1.8, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
|
||||
painter.setBrush(Qt.NoBrush)
|
||||
if self.action == "edit":
|
||||
path = QPainterPath(QPointF(4, 16))
|
||||
for x, y in ((15, 5), (19, 9), (8, 20), (3, 21), (4, 16)):
|
||||
path.lineTo(x, y)
|
||||
painter.drawPath(path)
|
||||
painter.drawLine(QPointF(13, 7), QPointF(17, 11))
|
||||
else:
|
||||
painter.drawLine(QPointF(4, 6), QPointF(20, 6))
|
||||
painter.drawRoundedRect(QRectF(6, 6, 12, 15), 1.5, 1.5)
|
||||
painter.drawPolyline(QPolygonF([QPointF(9, 6), QPointF(9, 3), QPointF(15, 3), QPointF(15, 6)]))
|
||||
painter.drawLine(QPointF(10, 10), QPointF(10, 17))
|
||||
painter.drawLine(QPointF(14, 10), QPointF(14, 17))
|
||||
painter.restore()
|
||||
|
||||
def pixmap(self, size, mode, state):
|
||||
pixmap = QPixmap(size)
|
||||
pixmap.fill(Qt.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
self.paint(painter, QRect(QPoint(), size), mode, state)
|
||||
painter.end()
|
||||
return pixmap
|
||||
|
||||
class SerialBus:
|
||||
"""ВЫСОКОУРОВНЕВЫЙ протокол для работы с серийной шиной (только через регистры)"""
|
||||
|
||||
@@ -320,12 +365,17 @@ class SerialTab(QWidget):
|
||||
|
||||
self.bus_write_btn = QPushButton("Записать в шину")
|
||||
self.bus_write_btn.clicked.connect(self.write_to_bus)
|
||||
bus_layout.addWidget(self.bus_write_btn, 1, 0, 1, 3)
|
||||
|
||||
self.bus_read_btn = QPushButton("Прочитать из шину")
|
||||
self.bus_read_btn.clicked.connect(self.read_from_bus)
|
||||
bus_layout.addWidget(self.bus_read_btn, 1, 3, 1, 3)
|
||||
bus_buttons = QHBoxLayout()
|
||||
bus_buttons.setSpacing(6)
|
||||
bus_buttons.addWidget(self.bus_write_btn)
|
||||
bus_buttons.addWidget(self.bus_read_btn)
|
||||
bus_buttons.addStretch(1)
|
||||
bus_layout.addLayout(bus_buttons, 1, 0, 1, 6)
|
||||
|
||||
bus_layout.setColumnStretch(6, 1)
|
||||
bus_group.setLayout(bus_layout)
|
||||
layout.addWidget(bus_group)
|
||||
|
||||
@@ -336,23 +386,34 @@ class SerialTab(QWidget):
|
||||
# Кнопка создания нового макроса
|
||||
self.create_macro_btn = QPushButton("Создать новый макрос")
|
||||
self.create_macro_btn.clicked.connect(self.create_macro)
|
||||
macros_layout.addWidget(self.create_macro_btn)
|
||||
self.stop_macro_btn = QPushButton("Остановить макрос")
|
||||
self.stop_macro_btn.setEnabled(False)
|
||||
self.stop_macro_btn.clicked.connect(self.stop_macro)
|
||||
macros_layout.addWidget(self.stop_macro_btn)
|
||||
macro_actions = QHBoxLayout()
|
||||
macro_actions.setSpacing(6)
|
||||
macro_actions.addWidget(self.create_macro_btn)
|
||||
macro_actions.addWidget(self.stop_macro_btn)
|
||||
macro_actions.addStretch(1)
|
||||
macros_layout.addLayout(macro_actions)
|
||||
for button in (self.bus_write_btn, self.bus_read_btn, self.create_macro_btn, self.stop_macro_btn):
|
||||
button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
||||
button.setFixedHeight(30)
|
||||
button.setStyleSheet("QPushButton { padding: 3px 10px; }")
|
||||
|
||||
# Прокручиваемая область для кнопок макросов
|
||||
self.macros_scroll_area = QScrollArea()
|
||||
self.macros_scroll_area.setWidgetResizable(True)
|
||||
# Устанавливаем фиксированную высоту, чтобы вместить 3-4 строки макросов
|
||||
self.macros_scroll_area.setMinimumHeight(150) # Высота для 3-4 строк
|
||||
self.macros_scroll_area.setMinimumHeight(120) # Высота для 3-4 строк
|
||||
|
||||
self.macros_scroll_content = QWidget()
|
||||
self.macros_scroll_layout = QGridLayout()
|
||||
self.macros_scroll_content.setLayout(self.macros_scroll_layout)
|
||||
self.macros_scroll_area.setWidget(self.macros_scroll_content)
|
||||
self.macros_scroll_layout.setAlignment(Qt.AlignTop)
|
||||
self.macros_scroll_layout.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||||
self.macros_scroll_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.macros_scroll_layout.setHorizontalSpacing(8)
|
||||
self.macros_scroll_layout.setVerticalSpacing(6)
|
||||
macros_layout.addWidget(self.macros_scroll_area)
|
||||
|
||||
macros_group.setLayout(macros_layout)
|
||||
@@ -375,6 +436,9 @@ class SerialTab(QWidget):
|
||||
results_layout.addWidget(self.bus_result_label, 1, 1)
|
||||
|
||||
# Создаем фрейм для результатов
|
||||
results_layout.setColumnStretch(2, 1)
|
||||
self.bus_read_value_label.setMinimumWidth(180)
|
||||
self.bus_result_label.setMaximumWidth(640)
|
||||
results_frame = QFrame()
|
||||
results_frame.setFrameShape(QFrame.StyledPanel)
|
||||
results_frame.setLayout(results_layout)
|
||||
@@ -470,8 +534,7 @@ class SerialTab(QWidget):
|
||||
|
||||
def add_macro_button_with_index(self, macro, index):
|
||||
"""Добавить кнопку макроса в интерфейс с указанным индексом"""
|
||||
# Изменяем расчет строк и колонок для 3 колонок
|
||||
items_per_row = 3 # Теперь 3 кнопки в строке
|
||||
items_per_row = 2
|
||||
row = len(self.macro_buttons) // items_per_row
|
||||
col = len(self.macro_buttons) % items_per_row
|
||||
|
||||
@@ -479,18 +542,20 @@ class SerialTab(QWidget):
|
||||
container = QWidget()
|
||||
container_layout = QHBoxLayout()
|
||||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
container_layout.setSpacing(4)
|
||||
container.setMaximumWidth(360)
|
||||
container.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
||||
|
||||
# Основная кнопка макроса
|
||||
macro_btn = QPushButton(macro['name'])
|
||||
macro_btn.setMinimumHeight(40)
|
||||
macro_btn.setFixedHeight(30)
|
||||
macro_btn.setMinimumWidth(120)
|
||||
macro_btn.setToolTip(macro["name"])
|
||||
macro_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #e0e0e0;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
""")
|
||||
|
||||
@@ -498,14 +563,22 @@ class SerialTab(QWidget):
|
||||
macro_btn.clicked.connect(partial(self.execute_macro_by_index, index))
|
||||
|
||||
# Кнопка редактирования
|
||||
edit_btn = QPushButton("✎")
|
||||
edit_btn = QPushButton()
|
||||
edit_btn.setIcon(QIcon(_MacroActionIcon("edit", edit_btn)))
|
||||
edit_btn.setIconSize(QSize(16, 16))
|
||||
edit_btn.setFixedSize(30, 30)
|
||||
edit_btn.setStyleSheet("QPushButton { padding: 0; }")
|
||||
edit_btn.setAccessibleName("Редактировать макрос")
|
||||
edit_btn.setToolTip("Редактировать макрос")
|
||||
edit_btn.clicked.connect(partial(self.edit_macro_by_index, index))
|
||||
|
||||
# Кнопка удаления
|
||||
delete_btn = QPushButton("✕")
|
||||
delete_btn = QPushButton()
|
||||
delete_btn.setIcon(QIcon(_MacroActionIcon("delete", delete_btn)))
|
||||
delete_btn.setIconSize(QSize(16, 16))
|
||||
delete_btn.setFixedSize(30, 30)
|
||||
delete_btn.setStyleSheet("QPushButton { padding: 0; }")
|
||||
delete_btn.setAccessibleName("Удалить макрос")
|
||||
delete_btn.setToolTip("Удалить макрос")
|
||||
delete_btn.clicked.connect(partial(self.delete_macro, index))
|
||||
|
||||
|
||||
64
python/set_devices/bin_compare.py
Normal file
64
python/set_devices/bin_compare.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Renderer-independent comparison of arbitrary binary files."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
|
||||
|
||||
def compare_sha256(image: bytes, readback: bytes):
|
||||
"""Compare a complete physical readback against an immutable BIN snapshot."""
|
||||
if not image or len(image) != len(readback):
|
||||
raise ValueError('Для SHA-256 нужен полный прочитанный диапазон размером с BIN.')
|
||||
expected = hashlib.sha256(image).hexdigest()
|
||||
actual = hashlib.sha256(readback).hexdigest()
|
||||
return expected == actual, expected, actual
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Comparison:
|
||||
left: bytes
|
||||
right: bytes
|
||||
offsets: tuple[int, ...]
|
||||
changed_bits: int
|
||||
|
||||
@property
|
||||
def changed_bytes(self) -> int:
|
||||
return len(self.offsets)
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return max(len(self.left), len(self.right))
|
||||
|
||||
|
||||
def compare(left: bytes, right: bytes) -> Comparison:
|
||||
"""Count bit differences in overlap; unmatched bytes count as changed bytes."""
|
||||
common = min(len(left), len(right))
|
||||
offsets = tuple(i for i in range(common) if left[i] != right[i])
|
||||
bits = sum(bin(left[i] ^ right[i]).count('1') for i in offsets)
|
||||
return Comparison(left, right, offsets + tuple(range(common, max(len(left), len(right)))), bits)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MultiComparison:
|
||||
images: tuple[bytes, ...]
|
||||
offsets: tuple[int, ...]
|
||||
pairs: tuple
|
||||
|
||||
@property
|
||||
def size(self): return max(map(len, self.images))
|
||||
|
||||
@property
|
||||
def changed_bytes(self): return len(self.offsets)
|
||||
|
||||
|
||||
def compare_many(images):
|
||||
"""Highlight offsets where any pair differs, including missing bytes."""
|
||||
images = tuple(images)
|
||||
if len(images) not in (1, 2, 3):
|
||||
raise ValueError('Выберите от 1 до 3 BIN-файлов.')
|
||||
if len(images) == 2:
|
||||
return compare(*images)
|
||||
pairs = tuple((a, b, compare(images[a], images[b]))
|
||||
for a in range(len(images)) for b in range(a + 1, len(images)))
|
||||
offsets = tuple(sorted({offset for _, _, result in pairs for offset in result.offsets}))
|
||||
return MultiComparison(images, offsets, pairs)
|
||||
107
python/set_devices/disassembly.py
Normal file
107
python/set_devices/disassembly.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""TI C28x tool adapter. Decode instructions using TI's own assembler tools."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def align_disassembly(*texts):
|
||||
"""Align TI listings by word offset even when instruction sizes differ."""
|
||||
listings = []
|
||||
for text in texts:
|
||||
if text.startswith('Ошибка:'):
|
||||
return texts
|
||||
listing = {}
|
||||
previous = None
|
||||
for line in text.splitlines():
|
||||
match = re.match(r'^([0-9A-Fa-f]{8})\s+', line)
|
||||
if match:
|
||||
previous = int(match.group(1), 16)
|
||||
listing[previous] = line
|
||||
elif previous is not None and line.strip():
|
||||
listing[previous] += ' ' + line.strip()
|
||||
if not listing:
|
||||
return texts
|
||||
listings.append(listing)
|
||||
addresses = sorted({address for listing in listings for address in listing})
|
||||
return tuple('\n'.join(listing.get(address, f'{address:08X} —') for address in addresses)
|
||||
for listing in listings)
|
||||
|
||||
|
||||
def find_ghidra(root):
|
||||
configured = os.environ.get('GHIDRA_INSTALL_DIR', '')
|
||||
if configured and (Path(configured) / 'support/analyzeHeadless.bat').is_file():
|
||||
return configured
|
||||
return next((str(path) for path in sorted(Path(root).glob('ghidra_*'), reverse=True)
|
||||
if (path / 'support/analyzeHeadless.bat').is_file()), '')
|
||||
|
||||
|
||||
def decompile_c28x(data: bytes, tools: str, base: int = 0) -> str:
|
||||
"""Run the configured Ghidra C28x processor and decompiler locally."""
|
||||
root = Path(tools)
|
||||
headless = root / 'support/analyzeHeadless.bat'
|
||||
if not tools or not headless.is_file():
|
||||
raise ValueError('Укажите папку Ghidra с установленным процессором TMS320C28x.')
|
||||
if not data or len(data) % 2 or len(data) > 262144:
|
||||
raise ValueError('Нужен непустой чётный диапазон до 256 КиБ.')
|
||||
if not 0 <= base <= 0x3fffff:
|
||||
raise ValueError('Адрес C28x должен быть в диапазоне 0…0x3FFFFF (в словах).')
|
||||
scripts = Path(__file__).parent / 'disassembly_scripts'
|
||||
env = dict(os.environ)
|
||||
jdks = sorted(path for path in root.parent.glob('jdk*')
|
||||
if path.is_dir() and (path / 'bin/java.exe').is_file())
|
||||
if jdks:
|
||||
env['JAVA_HOME'] = str(jdks[-1])
|
||||
with tempfile.TemporaryDirectory(prefix='setgui-decompile-') as folder:
|
||||
work = Path(folder)
|
||||
binary, output = work / 'range.bin', work / 'result.c'
|
||||
binary.write_bytes(data)
|
||||
arguments = [str(headless), str(work), 'analysis', '-import', str(binary),
|
||||
'-processor', 'TMS320C28x:LE:32:default', '-loader', 'BinaryLoader',
|
||||
'-loader-baseAddr', f'{base:x}', '-noanalysis', '-scriptPath', str(scripts),
|
||||
'-postScript', 'SetguiExportC.java', str(output), '-deleteProject']
|
||||
result = subprocess.run(arguments, env=env, capture_output=True, text=True,
|
||||
errors='replace', timeout=180, stdin=subprocess.DEVNULL,
|
||||
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
|
||||
if result.returncode or not output.is_file():
|
||||
raise RuntimeError((result.stderr + '\n' + result.stdout)[-6000:])
|
||||
text = output.read_text(encoding='utf-8')
|
||||
if not text.rstrip().endswith('/* SETGUI_DECOMPILE_SUCCESS */'):
|
||||
raise RuntimeError('Декомпиляция не завершена. ' + (result.stderr + '\n' + result.stdout)[-6000:])
|
||||
return text.removesuffix('/* SETGUI_DECOMPILE_SUCCESS */\n')
|
||||
|
||||
|
||||
def find_c28x_tools():
|
||||
candidates = sorted(Path('C:/ti').glob('ti-cgt-c2000_*/bin'), reverse=True)
|
||||
candidates += sorted(Path('C:/ti').glob('ccs*/ccs/tools/compiler/ti-cgt-c2000_*/bin'), reverse=True)
|
||||
return next((str(path) for path in candidates
|
||||
if (path / 'cl2000.exe').is_file() and (path / 'dis2000.exe').is_file()), '')
|
||||
|
||||
|
||||
def disassemble_c28x(data: bytes, tools: str) -> str:
|
||||
"""Wrap raw little-endian words in a temporary text section for dis2000."""
|
||||
if not data or len(data) % 2:
|
||||
raise ValueError('Для C28x нужен непустой диапазон с чётным числом байтов.')
|
||||
if len(data) > 262144:
|
||||
raise ValueError('Выберите диапазон не более 256 КиБ.')
|
||||
directory = Path(tools)
|
||||
for executable in ('cl2000.exe', 'dis2000.exe'):
|
||||
if not (directory / executable).is_file():
|
||||
raise ValueError(f'Не найден {executable} в {directory}')
|
||||
with tempfile.TemporaryDirectory(prefix='setgui-disasm-') as folder:
|
||||
root = Path(folder)
|
||||
source = root / 'dump.asm'
|
||||
source.write_text(' .text\n' + '\n'.join(
|
||||
f' .word 0x{int.from_bytes(data[i:i + 2], "little"):04X}'
|
||||
for i in range(0, len(data), 2)) + '\n', encoding='ascii')
|
||||
def run(arguments):
|
||||
result = subprocess.run(arguments, cwd=root, capture_output=True,
|
||||
text=True, errors='replace', timeout=30,
|
||||
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
|
||||
if result.returncode:
|
||||
raise RuntimeError(result.stderr or result.stdout or 'Ошибка TI tools')
|
||||
return result.stdout
|
||||
run([str(directory / 'cl2000.exe'), '--silicon_version=28', '--compile_only',
|
||||
'--output_file=dump.obj', str(source)])
|
||||
return run([str(directory / 'dis2000.exe'), '--data_as_text', 'dump.obj'])
|
||||
38
python/set_devices/disassembly_scripts/SetguiExportC.java
Normal file
38
python/set_devices/disassembly_scripts/SetguiExportC.java
Normal file
@@ -0,0 +1,38 @@
|
||||
// Local C pseudocode export for a raw, user-selected C28x code range.
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.app.decompiler.DecompInterface;
|
||||
import ghidra.app.decompiler.DecompileResults;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.FunctionIterator;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class SetguiExportC extends GhidraScript {
|
||||
public void run() throws Exception {
|
||||
Address entry = currentProgram.getMinAddress();
|
||||
disassemble(entry);
|
||||
createFunction(entry, null);
|
||||
analyzeAll(currentProgram);
|
||||
DecompInterface decompiler = new DecompInterface();
|
||||
try (PrintWriter out = new PrintWriter(getScriptArgs()[0], StandardCharsets.UTF_8.name())) {
|
||||
out.println("/* Ghidra C pseudocode. Recovered types/names may be incomplete. */");
|
||||
if (!decompiler.openProgram(currentProgram))
|
||||
throw new Exception(decompiler.getLastMessage());
|
||||
FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
|
||||
int count = 0;
|
||||
while (functions.hasNext() && !monitor.isCancelled()) {
|
||||
Function function = functions.next();
|
||||
if (function.isExternal()) continue;
|
||||
DecompileResults result = decompiler.decompileFunction(function, 10, monitor);
|
||||
out.println("/* Address: " + function.getEntryPoint() + " */");
|
||||
if (result.decompileCompleted() && result.getDecompiledFunction() != null) {
|
||||
out.println(result.getDecompiledFunction().getC());
|
||||
count++;
|
||||
} else out.println("/* Decompilation failed: " + result.getErrorMessage() + " */");
|
||||
}
|
||||
if (count == 0) throw new Exception("No functions decompiled. Select a range beginning at a function entry.");
|
||||
out.println("/* SETGUI_DECOMPILE_SUCCESS */");
|
||||
} finally { decompiler.dispose(); }
|
||||
}
|
||||
}
|
||||
40
python/set_devices/firmware_check.py
Normal file
40
python/set_devices/firmware_check.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Portable, read-only TMS firmware verification against an immutable BIN."""
|
||||
from protocan.tms2812 import build_upload_request, decode_upload_reply
|
||||
from .bin_compare import compare_sha256
|
||||
|
||||
|
||||
class TmsFirmwareCheck:
|
||||
def __init__(self, controller, word_address, image, block_size=256):
|
||||
self.image = bytes(image)
|
||||
if not 1 <= controller <= 255:
|
||||
raise ValueError("Для проверки нужен адрес контроллера 1..255")
|
||||
if not self.image or len(self.image) > 4 * 1024 * 1024:
|
||||
raise ValueError("Размер BIN должен быть 1..4194304 байт")
|
||||
if not 0 <= word_address <= 0xFFFFFFFF or word_address + (len(self.image) + 1) // 2 > 0x100000000:
|
||||
raise ValueError("Диапазон памяти вне 32-битного адресного пространства")
|
||||
if not 2 <= block_size <= 65534 or block_size % 2:
|
||||
raise ValueError("Размер блока должен быть чётным: 2..65534 байт")
|
||||
self.controller, self.word_address = controller, word_address
|
||||
self.block_size = block_size
|
||||
self.readback = bytearray()
|
||||
|
||||
@property
|
||||
def done(self):
|
||||
return len(self.readback) == len(self.image)
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
return min(self.block_size, len(self.image) - len(self.readback))
|
||||
|
||||
def request(self):
|
||||
if self.done:
|
||||
raise ValueError("Проверка уже завершена")
|
||||
return build_upload_request(self.controller, self.word_address + len(self.readback) // 2, self.count)
|
||||
|
||||
def accept(self, reply):
|
||||
if self.done:
|
||||
raise ValueError("Проверка уже завершена")
|
||||
self.readback.extend(decode_upload_reply(reply, self.controller, self.count))
|
||||
|
||||
def result(self):
|
||||
return compare_sha256(self.image, bytes(self.readback))
|
||||
110
python/set_devices/firmware_identity.py
Normal file
110
python/set_devices/firmware_identity.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Firmware-info v1 and the adjacent release passport (no device I/O)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from protocan.periph28335 import with_crc
|
||||
|
||||
MAX_IMAGE_SIZE = 199993
|
||||
|
||||
REGISTER_ADDRESS = 0x1200
|
||||
REGISTER_COUNT = 12
|
||||
|
||||
|
||||
def read_request(controller: int) -> bytes:
|
||||
if not 1 <= controller <= 255:
|
||||
raise ValueError("Для чтения версии нужен адрес контроллера 1..255")
|
||||
return with_crc(bytes((controller, 3)) + REGISTER_ADDRESS.to_bytes(2, 'big')
|
||||
+ REGISTER_COUNT.to_bytes(2, 'big'))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FirmwareIdentity:
|
||||
version: tuple[int, int, int]
|
||||
built_at: datetime
|
||||
build_id: str
|
||||
|
||||
def __post_init__(self):
|
||||
if (len(self.version) != 3 or any(type(v) is not int or not 0 <= v <= limit
|
||||
for v, limit in zip(self.version, (255, 255, 999)))
|
||||
or not 2000 <= self.built_at.year <= 2199
|
||||
or self.built_at.utcoffset() is None
|
||||
or self.built_at.microsecond
|
||||
or len(self.build_id) != 8
|
||||
or any(not 32 <= ord(c) <= 126 for c in self.build_id)):
|
||||
raise ValueError("Некорректные данные версии прошивки")
|
||||
|
||||
def describe(self):
|
||||
version = '.'.join(map(str, self.version))
|
||||
return f"{version} · {self.built_at:%Y-%m-%d %H:%M:%S} UTC · Build ID {self.build_id}"
|
||||
|
||||
|
||||
def decode_identity(words):
|
||||
if len(words) != REGISTER_COUNT or words[0] != 1:
|
||||
raise ValueError("Неизвестный формат версии (требуется firmware-info v1)")
|
||||
if any(type(w) is not int or not 0 <= w <= 65535 for w in words):
|
||||
raise ValueError("Некорректные регистры версии")
|
||||
try:
|
||||
stamp = datetime(words[4], words[5] >> 8, words[5] & 255,
|
||||
words[6] >> 8, words[6] & 255, words[7], tzinfo=timezone.utc)
|
||||
build_id = bytes(b for w in words[8:] for b in (w >> 8, w & 255)).decode('ascii')
|
||||
return FirmwareIdentity(tuple(words[1:4]), stamp, build_id)
|
||||
except (ValueError, UnicodeError) as error:
|
||||
raise ValueError("Некорректные данные версии: " + str(error)) from error
|
||||
|
||||
|
||||
def inspect_image(path: Path, data: bytes | None = None, *, expected_eeprom=None,
|
||||
expected_product="Balsam-167-PM35", expected_target="TMS320F28335",
|
||||
max_image_size=MAX_IMAGE_SIZE):
|
||||
"""Return None for legacy images; reject a present but invalid passport.
|
||||
|
||||
The caller may supply the exact bytes it will upload, avoiding a second
|
||||
read between hash verification and transmission.
|
||||
"""
|
||||
if data is None:
|
||||
with path.open('rb') as stream:
|
||||
data = stream.read(max_image_size + 1)
|
||||
if not 0 < len(data) <= max_image_size:
|
||||
raise ValueError(f"Размер бинарного файла должен быть 1..{max_image_size} байт")
|
||||
passport = Path(str(path) + '.manifest.json')
|
||||
if not passport.exists():
|
||||
return None
|
||||
try:
|
||||
with passport.open('r', encoding='utf-8-sig') as stream:
|
||||
raw = stream.read(65537)
|
||||
if len(raw) > 65536:
|
||||
raise ValueError("Слишком большой паспорт прошивки")
|
||||
meta = json.loads(raw)
|
||||
if (meta['schema'] != 1 or meta['identity_contract'] != 1
|
||||
or meta['identity_register_count'] != REGISTER_COUNT):
|
||||
raise ValueError("Неизвестный формат паспорта")
|
||||
if meta['product'] != expected_product or meta['target'] != expected_target:
|
||||
raise ValueError(f"Паспорт предназначен для другого устройства (ожидается {expected_product} / {expected_target})")
|
||||
if (type(meta['size_bytes']) is not int or meta['size_bytes'] != len(data)
|
||||
or meta['sha256'] != hashlib.sha256(data).hexdigest()):
|
||||
raise ValueError("BIN не соответствует паспорту: размер или SHA-256 не совпадает")
|
||||
if expected_eeprom is not None and meta.get('eeprom') is not None:
|
||||
if meta['eeprom'] != expected_eeprom:
|
||||
raise ValueError(f"BIN для {meta['eeprom']}, а выбрана EEPROM {expected_eeprom}")
|
||||
stamp = datetime.fromisoformat(meta['built_at_utc'].replace('Z', '+00:00'))
|
||||
if stamp.utcoffset() is None or stamp.utcoffset().total_seconds() != 0:
|
||||
raise ValueError("Дата паспорта должна быть в UTC")
|
||||
return FirmwareIdentity(tuple(meta['version']), stamp, meta['build_id'])
|
||||
except (KeyError, TypeError, ValueError, AttributeError) as error:
|
||||
raise ValueError("Ошибка паспорта: " + str(error)) from error
|
||||
|
||||
|
||||
def compare_identity(device, image):
|
||||
if device is None or image is None:
|
||||
return "Сравнение недоступно: нужны версия с платы и проверенный паспорт BIN."
|
||||
if device == image:
|
||||
return "Версия, дата сборки и Build ID совпадают. Содержимое памяти платы этим не проверяется."
|
||||
if image.version > device.version:
|
||||
return "Выбранная версия новее установленной."
|
||||
if image.version < device.version:
|
||||
return "Выбранная версия старее установленной (понижение версии)."
|
||||
return "Номер версии совпадает, но дата сборки или Build ID отличаются."
|
||||
@@ -8,6 +8,8 @@ See docs/tektronix-dpo4034.md for wiring, commands and scaling.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def resource_name(interface, address, board=0):
|
||||
@@ -52,18 +54,31 @@ class DPO4034:
|
||||
try:
|
||||
import pyvisa
|
||||
except ImportError as exc:
|
||||
raise RuntimeError("Установите PyVISA и NI-VISA или TekVISA") from exc
|
||||
if getattr(sys, "frozen", False):
|
||||
message = "В сборке SETGUI недоступна PyVISA. Используйте исправленную сборку EXE."
|
||||
else:
|
||||
message = "В Python-окружении недоступна PyVISA. Установите PyVISA (для Python 3.8: версия 1.14.1)."
|
||||
raise RuntimeError(message + " Причина: " + str(exc)) from exc
|
||||
factory = pyvisa.ResourceManager
|
||||
try:
|
||||
self.manager = factory()
|
||||
try:
|
||||
self.manager = factory()
|
||||
except (OSError, ValueError) as exc:
|
||||
if self._factory is not None:
|
||||
raise
|
||||
bits = struct.calcsize("P") * 8
|
||||
raise RuntimeError(
|
||||
f"Не удалось загрузить VISA-драйвер. Установите NI-VISA или TekVISA {bits}-bit "
|
||||
"с поддержкой вашей Windows (для Win7 нужна совместимая версия). Причина: " + str(exc)
|
||||
) from exc
|
||||
self.instrument = self.manager.open_resource(resource, open_timeout=timeout_ms)
|
||||
self.instrument.timeout = timeout_ms
|
||||
self.instrument.write_termination = "\n"
|
||||
self.instrument.read_termination = "\n"
|
||||
identity = self.instrument.query("*IDN?").strip()
|
||||
fields = identity.upper().split(",")
|
||||
if len(fields) < 2 or "TEKTRONIX" not in fields[0] or fields[1].strip() not in ("DPO4034", "MDO4034C"):
|
||||
raise ValueError(f"Ожидался Tektronix DPO4034 или MDO4034C, получено: {identity}")
|
||||
if len(fields) < 2 or "TEKTRONIX" not in fields[0] or fields[1].strip() not in ("DPO4034", "DPO4034B", "MDO4034C"):
|
||||
raise ValueError(f"Ожидался Tektronix DPO4034, DPO4034B или MDO4034C, получено: {identity}")
|
||||
self.instrument.write("HEADER OFF")
|
||||
return identity
|
||||
except Exception:
|
||||
|
||||
@@ -54,6 +54,8 @@ class ScopeSession:
|
||||
value = CaptureFrame(value, prepare_preview(value, args[3]))
|
||||
if automatic:
|
||||
state.consumed(status)
|
||||
elif action == "sync_settings":
|
||||
value = client.settings(*args)
|
||||
elif action == "export":
|
||||
filename, waves = args
|
||||
with open(filename, "w", newline="", encoding="utf-8-sig") as stream:
|
||||
|
||||
@@ -14,6 +14,7 @@ from dataclasses import dataclass
|
||||
from protocan import periph28335 as protocol
|
||||
|
||||
BASE = 0x1000
|
||||
PROTECTION = 0x1300 # Balsam 167: versioned voltage protection/test block.
|
||||
DATA = 0x1010
|
||||
LIVE = 0x1100
|
||||
EMU_CONTROL = 0x1200
|
||||
@@ -79,7 +80,7 @@ def request(controller: int, function: int, address: int, value: int, *, record_
|
||||
raise ValueError('Неверный размер записи логгера')
|
||||
capacity = {40: 800, 42: 760}[record_words]
|
||||
if function == 3:
|
||||
valid = ((address == BASE and value == 16) or
|
||||
valid = ((address == PROTECTION and value == 8) or (address == BASE and value == 16) or
|
||||
(address == DATA and 1 <= value <= 120 and (value % WORDS == 0 or value in (42, 84))) or (address == LIVE and value == 124) or (address == EMU_CONTROL and value == 4)
|
||||
or (address == FAST_CAPABILITY and value == 4)
|
||||
or (FAST_DATA <= address < FAST_DATA + capacity * record_words
|
||||
@@ -87,7 +88,9 @@ def request(controller: int, function: int, address: int, value: int, *, record_
|
||||
and 1 <= value <= 120 and value % record_words == 0
|
||||
and address + value <= FAST_DATA + capacity * record_words))
|
||||
else:
|
||||
valid = function == 6 and (address in (BASE, BASE + 1, BASE + 2, BASE + 3) or
|
||||
valid = function == 6 and ((address == PROTECTION + 2 and value in (0, 1, 2, 3)) or
|
||||
(address == PROTECTION + 3 and 1 <= value <= 60000) or
|
||||
address in (BASE, BASE + 1, BASE + 2, BASE + 3) or
|
||||
(address == EMU_CONTROL + 2 and value in (0, 1)))
|
||||
if not valid:
|
||||
raise ValueError('Недопустимый запрос логгера УМП')
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
import struct
|
||||
from set_devices.ump_logger import BASE, DATA, LIVE, EMU_CONTROL
|
||||
from set_devices.ump_logger import BASE, DATA, LIVE, EMU_CONTROL, PROTECTION
|
||||
|
||||
MARKER = 0x1F00
|
||||
MAX_WORDS = 124
|
||||
@@ -26,10 +26,12 @@ def request_ids(mode):
|
||||
def build_request(mode, token, function, address, value):
|
||||
if not 1 <= token <= 255:
|
||||
raise ValueError('Неверный номер CAN-запроса')
|
||||
valid = ((function == 3 and ((address == BASE and value == 16) or
|
||||
valid = ((function == 3 and ((address == PROTECTION and value == 8) or (address == BASE and value == 16) or
|
||||
(address == DATA and value in (40, 80, 120, 42, 84)) or
|
||||
(address == LIVE and value == MAX_WORDS) or (address == EMU_CONTROL and value == 4))) or
|
||||
(function == 6 and ((BASE <= address <= BASE+3 and 0 <= value <= 65535) or
|
||||
(address == PROTECTION+2 and value in (0, 1, 2, 3)) or
|
||||
(address == PROTECTION+3 and 1 <= value <= 60000) or
|
||||
(address == EMU_CONTROL+2 and value in (0, 1)))))
|
||||
if not valid:
|
||||
raise ValueError('Недопустимый запрос CAN-логгера')
|
||||
|
||||
@@ -24,7 +24,8 @@ def sample(time_ms, period_ms=20, *, repeat=True):
|
||||
else:
|
||||
current = 4
|
||||
|
||||
words = [0] * codec.WORDS
|
||||
# Extended logger record includes both DC-link voltage measurements.
|
||||
words = [0] * 42
|
||||
words[0], words[1] = time_ms & 65535, (time_ms >> 16) & 65535
|
||||
words[2] = 1 | (0x100 if 5000 <= phase < 5000 + period_ms else 0)
|
||||
words[3] = (int(started) << 7) | (int(reset) << 6)
|
||||
@@ -42,6 +43,14 @@ def sample(time_ms, period_ms=20, *, repeat=True):
|
||||
words[18:24] = [0xE07F, words[3], 6, 16, (time_ms // 100) & 65535, 0]
|
||||
words[24:31] = [6, 16, 500, 3500, 2000, 500, 3500]
|
||||
words[31:36] = [ramp if started else 0, 2500, 60, 160, round(ramp * 1000 / 2500)]
|
||||
if started:
|
||||
charge = min(1.0, (phase - 1000) / 1500)
|
||||
elif phase >= 5000:
|
||||
charge = max(0.0, 1 - (phase - 5000) / 800)
|
||||
else:
|
||||
charge = 0.0
|
||||
words[40] = round(charge * (600 + 3 * math.sin(phase / 80)))
|
||||
words[41] = round(charge * (590 + 4 * math.sin(phase / 95 + .7)))
|
||||
return codec.sample(words)
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,17 @@ import os
|
||||
import unittest
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtTest import QTest
|
||||
from PySide6.QtCore import QEventLoop, QTimer
|
||||
from pysetterminal.rawprotocol import RawProtocolWidget
|
||||
from pysetterminal.serialbus import SerialBus, SerialTab
|
||||
|
||||
|
||||
def process_events(milliseconds):
|
||||
loop = QEventLoop()
|
||||
QTimer.singleShot(milliseconds, loop.quit)
|
||||
loop.exec()
|
||||
|
||||
|
||||
class PendingProtocol:
|
||||
def __init__(self):
|
||||
self.requests = []
|
||||
@@ -39,11 +45,11 @@ class MacroTests(unittest.TestCase):
|
||||
def test_scheduled_execution_returns_and_can_be_cancelled(self):
|
||||
self.tab.execute_macro_commands([self.command])
|
||||
self.assertEqual(self.protocol.requests, [])
|
||||
QTest.qWait(20)
|
||||
process_events(20)
|
||||
self.assertEqual(self.protocol.requests, [(0x200B, 0x35)])
|
||||
self.assertTrue(self.tab.stop_macro_btn.isEnabled())
|
||||
self.tab.stop_macro()
|
||||
QTest.qWait(20)
|
||||
process_events(20)
|
||||
self.assertFalse(self.tab._macro_running)
|
||||
self.assertTrue(self.tab.bus_read_btn.isEnabled())
|
||||
self.assertEqual(len(self.protocol.requests), 1)
|
||||
@@ -52,20 +58,20 @@ class MacroTests(unittest.TestCase):
|
||||
self.tab.execute_macro_commands([self.command])
|
||||
self.tab.stop_macro()
|
||||
self.tab.execute_macro_commands([self.command])
|
||||
QTest.qWait(20)
|
||||
process_events(20)
|
||||
self.assertEqual(len(self.protocol.requests), 1)
|
||||
self.protocol.callback(False, "CRC error")
|
||||
QTest.qWait(20)
|
||||
process_events(20)
|
||||
self.assertFalse(self.tab._macro_running)
|
||||
self.assertEqual(len(self.protocol.requests), 1)
|
||||
|
||||
def test_completion_runs_next_step_and_updates_value(self):
|
||||
self.tab.execute_macro_commands([self.command])
|
||||
QTest.qWait(20)
|
||||
process_events(20)
|
||||
self.protocol.callback(True, {'raw': b"ack"})
|
||||
QTest.qWait(20)
|
||||
process_events(20)
|
||||
self.assertEqual(self.protocol.requests[-1], (0x200F, None))
|
||||
self.protocol.callback(True, {'value': 0x1234, 'raw_response': {'raw': b"reply"}})
|
||||
QTest.qWait(20)
|
||||
process_events(20)
|
||||
self.assertFalse(self.tab._macro_running)
|
||||
self.assertEqual(self.tab.bus_read_value_label.text(), "0x1234 (4660)")
|
||||
|
||||
Reference in New Issue
Block a user